Compare commits

..

7 Commits

Author SHA1 Message Date
Christopher Haster
f935fc0be6 Fixed lookahead overflow and removed unbounded lookahead pointers
As pointed out by davidefer, the lookahead pointer modular arithmetic
does not work around integer overflow when the pointer size is not a
multiple of the block count.

To avoid overflow problems, the easy solution is to stop trying to
work around integer overflows and keep the lookahead offset inside the
block device. To make this work, the ack was modified into a resetable
counter that is decremented every block allocation.

As a plus, quite a bit of the allocation logic ended up simplified.
2018-04-10 15:14:27 -05:00
Christopher Haster
6a89ecba39 Added test for lookahead overflow 2018-04-09 14:37:35 -05:00
Christopher Haster
89a7630d84 Fixed issue with lookahead trusting old lookahead blocks
One of the big simplifications in littlefs's implementation is the
complete lack of tracking free blocks, allowing operations to simply
drop blocks that are no longer in use.

However, this means the lookahead buffer can easily contain outdated
blocks that were previously deleted. This is usually fine, as littlefs
will rescan the storage if it can't find a free block in the lookahead
buffer, but after changes that caused littlefs to more conservatively
respect the alloc acks (e611cf5), any scanned blocks after an ack would
be incorrectly trusted.

The fix is to eagerly scan ahead in the lookahead when we allocate so
that alloc acks are better able to discredit old lookahead blocks. Since
usually alloc acks are tightly coupled to allocations of one or two blocks,
this allows littlefs to properly rescan every set of allocations.

This may still be a concern if there is a long series of worn out
blocks, but in the worst case littlefs will conservatively avoid using
blocks it's not sure about.

Found by davidefer
2018-04-09 14:37:35 -05:00
Christopher Haster
43eac3083b Renamed test_parallel tests to test_interespersed
The name test_parallel gave off the incorrect impression that these
tests are multithreaded.
2018-04-08 17:31:09 -05:00
Christopher Haster
dbc3cb1798 Fixed Travis rate-limit issue with Github requests
Using credentials avoids rate-limiting based on Travis's IP address
2018-04-08 17:31:09 -05:00
Christopher Haster
93ece2e87a Removed outdated note about moves and powerloss 2018-04-08 17:31:05 -05:00
Christopher Haster
d9c076d909 Removed the uninitialized read for invalid superblocks 2018-03-19 00:39:40 -05:00
6 changed files with 348 additions and 332 deletions

View File

@@ -35,7 +35,7 @@ script:
if [ "$TRAVIS_TEST_RESULT" -eq 0 ]
then
CURR=$(tail -n1 sizes | awk '{print $1}')
PREV=$(curl https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/master \
PREV=$(curl -u $GEKY_BOT_STATUSES https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/master \
| jq -re "select(.sha != \"$TRAVIS_COMMIT\")
| .statuses[] | select(.context == \"$STAGE/$NAME\").description
| capture(\"code size is (?<size>[0-9]+)\").size" \
@@ -165,7 +165,8 @@ jobs:
\"name\": \"$LFS_VERSION\"
}"
RELEASE=$(
curl -f https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases/tags/$LFS_VERSION
curl -f -u $GEKY_BOT_RELEASES \
https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases/tags/$LFS_VERSION
)
CHANGES=$(
git log --oneline $LFS_PREV_VERSION.. --grep='^Merge' --invert-grep

View File

@@ -33,8 +33,8 @@ size: $(OBJ)
$(SIZE) -t $^
.SUFFIXES:
test: test_format test_dirs test_files test_seek test_truncate test_parallel \
test_alloc test_paths test_orphan test_move test_corrupt
test: test_format test_dirs test_files test_seek test_truncate \
test_interspersed test_alloc test_paths test_orphan test_move test_corrupt
test_%: tests/test_%.sh
ifdef QUIET
@./$< | sed -n '/^[-=]/p'

488
lfs.c
View File

@@ -270,8 +270,7 @@ int lfs_deorphan(lfs_t *lfs);
static int lfs_alloc_lookahead(void *p, lfs_block_t block) {
lfs_t *lfs = p;
lfs_block_t off = (((lfs_soff_t)(block - lfs->free.begin)
% (lfs_soff_t)(lfs->cfg->block_count))
lfs_block_t off = ((block - lfs->free.off)
+ lfs->cfg->block_count) % lfs->cfg->block_count;
if (off < lfs->free.size) {
@@ -283,27 +282,39 @@ static int lfs_alloc_lookahead(void *p, lfs_block_t block) {
static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) {
while (true) {
while (lfs->free.off != lfs->free.size) {
lfs_block_t off = lfs->free.off;
lfs->free.off += 1;
while (lfs->free.i != lfs->free.size) {
lfs_block_t off = lfs->free.i;
lfs->free.i += 1;
lfs->free.ack -= 1;
if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) {
// found a free block
*block = (lfs->free.begin + off) % lfs->cfg->block_count;
*block = (lfs->free.off + off) % lfs->cfg->block_count;
// eagerly find next off so an alloc ack can
// discredit old lookahead blocks
while (lfs->free.i != lfs->free.size &&
(lfs->free.buffer[lfs->free.i / 32]
& (1U << (lfs->free.i % 32)))) {
lfs->free.i += 1;
lfs->free.ack -= 1;
}
return 0;
}
}
// check if we have looked at all blocks since last ack
if (lfs->free.off == lfs->free.ack - lfs->free.begin) {
LFS_WARN("No more free space %d", lfs->free.off + lfs->free.begin);
//if (lfs->free.i == lfs->free.ack - lfs->free.off) {
if (lfs->free.ack == 0) {
LFS_WARN("No more free space %d", lfs->free.i + lfs->free.off);
return LFS_ERR_NOSPC;
}
lfs->free.begin += lfs->free.size;
lfs->free.size = lfs_min(lfs->cfg->lookahead,
lfs->free.ack - lfs->free.begin);
lfs->free.off = 0;
lfs->free.off = (lfs->free.off + lfs->free.size)
% lfs->cfg->block_count;
lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->free.ack);
lfs->free.i = 0;
// find mask of free blocks from tree
memset(lfs->free.buffer, 0, lfs->cfg->lookahead/8);
@@ -315,7 +326,7 @@ static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) {
}
static void lfs_alloc_ack(lfs_t *lfs) {
lfs->free.ack = lfs->free.off-1 + lfs->free.begin + lfs->cfg->block_count;
lfs->free.ack = lfs->cfg->block_count;
}
@@ -386,6 +397,10 @@ static inline bool lfs_pairsync(
(paira[0] == pairb[1] && paira[1] == pairb[0]);
}
static inline lfs_size_t lfs_entry_size(const lfs_entry_t *entry) {
return 4 + entry->d.elen + entry->d.alen + entry->d.nlen;
}
static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) {
// allocate pair of dir blocks
for (int i = 0; i < 2; i++) {
@@ -469,93 +484,27 @@ static int lfs_dir_fetch(lfs_t *lfs,
return 0;
}
struct lfs_commit {
uint32_t crc;
lfs_block_t block;
lfs_off_t off;
};
static int lfs_commit(lfs_t *lfs, struct lfs_commit *c,
const void *data, lfs_size_t size) {
lfs_crc(&c->crc, data, size);
int err = lfs_bd_prog(lfs, c->block, c->off, data, size);
c->off += size;
return err;
}
struct lfs_region {
lfs_off_t off;
lfs_ssize_t diff;
int (*commit)(lfs_t *lfs, struct lfs_commit *c,
const void *data, lfs_size_t size);
const void *data;
lfs_size_t size;
struct lfs_region *next;
lfs_off_t oldoff;
lfs_size_t oldlen;
const void *newdata;
lfs_size_t newlen;
};
static int lfs_commit_mem(lfs_t *lfs, struct lfs_commit *c,
const void *data, lfs_size_t size) {
return lfs_commit(lfs, c, data, size);
}
struct lfs_commit_disk {
lfs_block_t block;
lfs_off_t off;
struct lfs_region *regions;
};
static int lfs_commit_disk(lfs_t *lfs, struct lfs_commit *c,
const void *p, lfs_size_t size) {
const struct lfs_commit_disk *d = p;
struct lfs_region *r = d->regions;
lfs_off_t off = 0;
while (true) {
if (r && r->off == off) {
lfs_off_t orig = c->off;
int err = r->commit(lfs, c, r->data, r->size);
if (err) {
return err;
}
off += (c->off - orig) - r->diff;
r = r->next;
} else if (off < size) {
uint8_t data;
int err = lfs_bd_read(lfs, d->block, d->off + off, &data, 1);
if (err) {
return err;
}
err = lfs_commit(lfs, c, &data, 1);
if (err) {
return err;
}
off += 1;
} else {
return 0;
}
}
}
static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir,
struct lfs_region *regions) {
// state for copying over
const lfs_block_t oldpair[2] = {dir->pair[1], dir->pair[0]};
lfs_size_t oldsize = (0x7fffffff & dir->d.size) - 4;
bool relocated = false;
const struct lfs_region *regions, int count) {
// increment revision count
dir->d.rev += 1;
// keep pairs in order such that pair[0] is most recent
lfs_pairswap(dir->pair);
for (struct lfs_region *r = regions; r; r = r->next) {
dir->d.size += r->diff;
for (int i = 0; i < count; i++) {
dir->d.size += regions[i].newlen - regions[i].oldlen;
}
const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]};
bool relocated = false;
while (true) {
if (true) {
int err = lfs_bd_erase(lfs, dir->pair[0]);
@@ -566,19 +515,10 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir,
return err;
}
struct lfs_commit c = {
.crc = 0xffffffff,
.block = dir->pair[0],
.off = 0,
};
uint32_t crc = 0xffffffff;
lfs_dir_tole32(&dir->d);
err = lfs_commit_disk(lfs, &c, &(struct lfs_commit_disk){
oldpair[1], 0,
&(struct lfs_region){
0, 0,
lfs_commit_mem, &dir->d, sizeof(dir->d),
regions}}, oldsize);
lfs_crc(&crc, &dir->d, sizeof(dir->d));
err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d));
lfs_dir_fromle32(&dir->d);
if (err) {
if (err == LFS_ERR_CORRUPT) {
@@ -587,9 +527,48 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir,
return err;
}
c.crc = lfs_tole32(c.crc);
err = lfs_bd_prog(lfs, dir->pair[0], c.off, &c.crc, 4);
c.crc = lfs_fromle32(c.crc);
int i = 0;
lfs_off_t oldoff = sizeof(dir->d);
lfs_off_t newoff = sizeof(dir->d);
while (newoff < (0x7fffffff & dir->d.size)-4) {
if (i < count && regions[i].oldoff == oldoff) {
lfs_crc(&crc, regions[i].newdata, regions[i].newlen);
err = lfs_bd_prog(lfs, dir->pair[0],
newoff, regions[i].newdata, regions[i].newlen);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
oldoff += regions[i].oldlen;
newoff += regions[i].newlen;
i += 1;
} else {
uint8_t data;
err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1);
if (err) {
return err;
}
lfs_crc(&crc, &data, 1);
err = lfs_bd_prog(lfs, dir->pair[0], newoff, &data, 1);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
oldoff += 1;
newoff += 1;
}
}
crc = lfs_tole32(crc);
err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4);
crc = lfs_fromle32(crc);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
@@ -613,13 +592,12 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir,
return err;
}
if (ncrc != c.crc) {
if (ncrc != crc) {
goto relocate;
}
}
break;
relocate:
//commit was corrupted
LFS_DEBUG("Bad block at %d", dir->pair[0]);
@@ -662,18 +640,29 @@ relocate:
return 0;
}
static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir,
lfs_entry_t *entry, const void *data) {
lfs_entry_tole32(&entry->d);
int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)},
{entry->off+sizeof(entry->d), entry->d.nlen, data, entry->d.nlen}
}, data ? 2 : 1);
lfs_entry_fromle32(&entry->d);
return err;
}
static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
lfs_entry_t *entry, struct lfs_region *regions) {
lfs_entry_t *entry, const void *data) {
// check if we fit, if top bit is set we do not and move on
while (true) {
if ((0x7fffffff & dir->d.size) + entry->size <= lfs->cfg->block_size) {
if (dir->d.size + lfs_entry_size(entry) <= lfs->cfg->block_size) {
entry->off = dir->d.size - 4;
for (struct lfs_region *r = regions; r; r = r->next) {
r->off += entry->off;
}
lfs_entry_tole32(&entry->d);
int err = lfs_dir_commit(lfs, dir, regions);
int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, 0, &entry->d, sizeof(entry->d)},
{entry->off, 0, data, entry->d.nlen}
}, 2);
lfs_entry_fromle32(&entry->d);
return err;
}
@@ -689,12 +678,11 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
dir->d.tail[0] = olddir.d.tail[0];
dir->d.tail[1] = olddir.d.tail[1];
entry->off = dir->d.size - 4;
for (struct lfs_region *r = regions; r; r = r->next) {
r->off += entry->off;
}
lfs_entry_tole32(&entry->d);
err = lfs_dir_commit(lfs, dir, regions);
err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, 0, &entry->d, sizeof(entry->d)},
{entry->off, 0, data, entry->d.nlen}
}, 2);
lfs_entry_fromle32(&entry->d);
if (err) {
return err;
@@ -703,7 +691,7 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
olddir.d.size |= 0x80000000;
olddir.d.tail[0] = dir->pair[0];
olddir.d.tail[1] = dir->pair[1];
return lfs_dir_commit(lfs, &olddir, NULL);
return lfs_dir_commit(lfs, &olddir, NULL, 0);
}
int err = lfs_dir_fetch(lfs, dir, dir->d.tail);
@@ -713,10 +701,10 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
}
}
static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir,
const lfs_entry_t *entry) {
static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
// check if we should just drop the directory block
if ((dir->d.size & 0x7fffffff) == sizeof(dir->d)+4 + entry->size) {
if ((dir->d.size & 0x7fffffff) == sizeof(dir->d)+4
+ lfs_entry_size(entry)) {
lfs_dir_t pdir;
int res = lfs_pred(lfs, dir->pair, &pdir);
if (res < 0) {
@@ -727,15 +715,14 @@ static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir,
pdir.d.size &= dir->d.size | 0x7fffffff;
pdir.d.tail[0] = dir->d.tail[0];
pdir.d.tail[1] = dir->d.tail[1];
return lfs_dir_commit(lfs, &pdir, NULL);
return lfs_dir_commit(lfs, &pdir, NULL, 0);
}
}
// shift out the entry
int err = lfs_dir_commit(lfs, dir,
&(struct lfs_region){
entry->off, -entry->size,
lfs_commit_mem, NULL, 0});
int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, lfs_entry_size(entry), NULL, 0},
}, 1);
if (err) {
return err;
}
@@ -747,7 +734,7 @@ static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir,
f->pair[0] = 0xffffffff;
f->pair[1] = 0xffffffff;
} else if (f->poff > entry->off) {
f->poff -= entry->size;
f->poff -= lfs_entry_size(entry);
}
}
}
@@ -755,8 +742,8 @@ static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir,
for (lfs_dir_t *d = lfs->dirs; d; d = d->next) {
if (lfs_paircmp(d->pair, dir->pair) == 0) {
if (d->off > entry->off) {
d->off -= entry->size;
d->pos -= entry->size;
d->off -= lfs_entry_size(entry);
d->pos -= lfs_entry_size(entry);
}
}
}
@@ -764,82 +751,6 @@ static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir,
return 0;
}
static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir,
lfs_entry_t *entry, struct lfs_region *regions) {
lfs_ssize_t diff = 0;
for (struct lfs_region *r = regions; r; r = r->next) {
diff += r->diff;
}
// do we still fit?
if ((0x7fffffff & dir->d.size) + diff <= lfs->cfg->block_size) {
for (struct lfs_region *r = regions; r; r = r->next) {
r->off += entry->off;
}
lfs_entry_tole32(&entry->d);
int err = lfs_dir_commit(lfs, dir, regions);
lfs_entry_fromle32(&entry->d);
if (err) {
return err;
}
// shift over any files/directories that are affected
for (lfs_file_t *f = lfs->files; f; f = f->next) {
if (lfs_paircmp(f->pair, dir->pair) == 0) {
if (f->poff > entry->off) {
f->poff += diff;
}
}
}
for (lfs_dir_t *d = lfs->dirs; d; d = d->next) {
if (lfs_paircmp(d->pair, dir->pair) == 0) {
if (d->off > entry->off) {
d->off += diff;
d->pos += diff;
}
}
}
} else {
lfs_dir_t olddir = *dir;
lfs_entry_t oldentry = {
.off = entry->off,
.size = entry->size - diff,
.d.type = entry->d.type | LFS_STRUCT_MOVED,
};
// mark as moving
int err = lfs_dir_commit(lfs, &olddir,
&(struct lfs_region){
oldentry.off, 0,
lfs_commit_mem, &oldentry.d.type, 1});
if (err) {
return err;
}
// append updated entry
lfs_entry_tole32(&entry->d);
err = lfs_dir_append(lfs, dir, entry,
&(struct lfs_region){
0, +entry->size,
lfs_commit_disk, &(struct lfs_commit_disk){
olddir.pair[0], entry->off, regions}, oldentry.size});
lfs_entry_fromle32(&entry->d);
if (err) {
return err;
}
// remove old entry
err = lfs_dir_remove(lfs, dir, &oldentry);
if (err) {
return err;
}
}
return 0;
}
static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)-4) {
if (!(0x80000000 & dir->d.size)) {
@@ -864,9 +775,8 @@ static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
}
entry->off = dir->off;
entry->size = 4 + entry->d.elen + entry->d.alen + entry->d.nlen;
dir->off += entry->size;
dir->pos += entry->size;
dir->off += lfs_entry_size(entry);
dir->pos += lfs_entry_size(entry);
return 0;
}
@@ -884,7 +794,10 @@ static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir,
// special case for root dir
if (pathname[0] == '\0') {
*entry = (lfs_entry_t){
.d.type = LFS_STRUCT_DIR | LFS_TYPE_DIR,
.d.type = LFS_TYPE_DIR,
.d.elen = sizeof(entry->d) - 4,
.d.alen = 0,
.d.nlen = 0,
.d.u.dir[0] = lfs->root[0],
.d.u.dir[1] = lfs->root[1],
};
@@ -932,14 +845,14 @@ static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir,
return err;
}
if (((0x7f & entry->d.type) != (LFS_STRUCT_CTZ | LFS_TYPE_REG) &&
(0x7f & entry->d.type) != (LFS_STRUCT_DIR | LFS_TYPE_DIR)) ||
if (((0x7f & entry->d.type) != LFS_TYPE_REG &&
(0x7f & entry->d.type) != LFS_TYPE_DIR) ||
entry->d.nlen != pathlen) {
continue;
}
int res = lfs_bd_cmp(lfs, dir->pair[0],
entry->off + entry->size - pathlen,
entry->off + 4+entry->d.elen+entry->d.alen,
pathname, pathlen);
if (res < 0) {
return res;
@@ -952,13 +865,13 @@ static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir,
}
// check that entry has not been moved
if (entry->d.type & LFS_STRUCT_MOVED) {
if (entry->d.type & 0x80) {
int moved = lfs_moved(lfs, &entry->d.u);
if (moved < 0 || moved) {
return (moved < 0) ? moved : LFS_ERR_NOENT;
}
entry->d.type &= ~LFS_STRUCT_MOVED;
entry->d.type &= ~0x80;
}
pathname += pathlen;
@@ -968,7 +881,7 @@ static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir,
}
// continue on if we hit a directory
if ((0xf & entry->d.type) != LFS_TYPE_DIR) {
if (entry->d.type != LFS_TYPE_DIR) {
return LFS_ERR_NOTDIR;
}
@@ -1014,29 +927,22 @@ int lfs_mkdir(lfs_t *lfs, const char *path) {
dir.d.tail[0] = cwd.d.tail[0];
dir.d.tail[1] = cwd.d.tail[1];
err = lfs_dir_commit(lfs, &dir, NULL);
err = lfs_dir_commit(lfs, &dir, NULL, 0);
if (err) {
return err;
}
entry.d.type = LFS_STRUCT_DIR | LFS_TYPE_DIR;
entry.d.type = LFS_TYPE_DIR;
entry.d.elen = sizeof(entry.d) - 4;
entry.d.alen = 0;
entry.d.nlen = strlen(path);
entry.d.u.dir[0] = dir.pair[0];
entry.d.u.dir[1] = dir.pair[1];
entry.size = 4 + entry.d.elen + entry.d.alen + entry.d.nlen;
cwd.d.tail[0] = dir.pair[0];
cwd.d.tail[1] = dir.pair[1];
err = lfs_dir_append(lfs, &cwd, &entry,
&(struct lfs_region){
0, +sizeof(entry.d),
lfs_commit_mem, &entry.d, sizeof(entry.d),
&(struct lfs_region){
0, +entry.d.nlen,
lfs_commit_mem, path, entry.d.nlen}});
err = lfs_dir_append(lfs, &cwd, &entry, path);
if (err) {
return err;
}
@@ -1058,7 +964,7 @@ int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) {
err = lfs_dir_find(lfs, dir, &entry, &path);
if (err) {
return err;
} else if (entry.d.type != (LFS_STRUCT_DIR | LFS_TYPE_DIR)) {
} else if (entry.d.type != LFS_TYPE_DIR) {
return LFS_ERR_NOTDIR;
}
@@ -1116,13 +1022,13 @@ int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) {
return (err == LFS_ERR_NOENT) ? 0 : err;
}
if ((0x7f & entry.d.type) != (LFS_STRUCT_CTZ | LFS_TYPE_REG) &&
(0x7f & entry.d.type) != (LFS_STRUCT_DIR | LFS_TYPE_DIR)) {
if ((0x7f & entry.d.type) != LFS_TYPE_REG &&
(0x7f & entry.d.type) != LFS_TYPE_DIR) {
continue;
}
// check that entry has not been moved
if (entry.d.type & LFS_STRUCT_MOVED) {
if (entry.d.type & 0x80) {
int moved = lfs_moved(lfs, &entry.d.u);
if (moved < 0) {
return moved;
@@ -1132,19 +1038,19 @@ int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) {
continue;
}
entry.d.type &= ~LFS_STRUCT_MOVED;
entry.d.type &= ~0x80;
}
break;
}
info->type = 0xf & entry.d.type;
info->type = entry.d.type;
if (info->type == LFS_TYPE_REG) {
info->size = entry.d.u.file.size;
}
int err = lfs_bd_read(lfs, dir->pair[0],
entry.off + entry.size - entry.d.nlen,
entry.off + 4+entry.d.elen+entry.d.alen,
info->name, entry.d.nlen);
if (err) {
return err;
@@ -1414,25 +1320,17 @@ int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
}
// create entry to remember name
entry.d.type = LFS_STRUCT_CTZ | LFS_TYPE_REG;
entry.d.type = LFS_TYPE_REG;
entry.d.elen = sizeof(entry.d) - 4;
entry.d.alen = 0;
entry.d.nlen = strlen(path);
entry.d.u.file.head = 0xffffffff;
entry.d.u.file.size = 0;
entry.size = 4 + entry.d.elen + entry.d.alen + entry.d.nlen;
err = lfs_dir_append(lfs, &cwd, &entry,
&(struct lfs_region){
0, +sizeof(entry.d),
lfs_commit_mem, &entry.d, sizeof(entry.d),
&(struct lfs_region){
0, +entry.d.nlen,
lfs_commit_mem, path, entry.d.nlen}});
err = lfs_dir_append(lfs, &cwd, &entry, path);
if (err) {
return err;
}
} else if ((0xf & entry.d.type) == LFS_TYPE_DIR) {
} else if (entry.d.type == LFS_TYPE_DIR) {
return LFS_ERR_ISDIR;
} else if (flags & LFS_O_EXCL) {
return LFS_ERR_EXIST;
@@ -1640,16 +1538,11 @@ int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) {
return err;
}
LFS_ASSERT(entry.d.type == (LFS_STRUCT_CTZ | LFS_TYPE_REG));
LFS_ASSERT(entry.d.type == LFS_TYPE_REG);
entry.d.u.file.head = file->head;
entry.d.u.file.size = file->size;
lfs_entry_tole32(&entry.d);
err = lfs_dir_update(lfs, &cwd, &entry,
&(struct lfs_region){
0, 0,
lfs_commit_mem, &entry.d, sizeof(entry.d)});
lfs_entry_fromle32(&entry.d);
err = lfs_dir_update(lfs, &cwd, &entry, NULL);
if (err) {
return err;
}
@@ -1934,7 +1827,7 @@ int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) {
}
memset(info, 0, sizeof(*info));
info->type = 0xf & entry.d.type;
info->type = entry.d.type;
if (info->type == LFS_TYPE_REG) {
info->size = entry.d.u.file.size;
}
@@ -1943,7 +1836,7 @@ int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) {
strcpy(info->name, "/");
} else {
err = lfs_bd_read(lfs, cwd.pair[0],
entry.off + entry.size - entry.d.nlen,
entry.off + 4+entry.d.elen+entry.d.alen,
info->name, entry.d.nlen);
if (err) {
return err;
@@ -1975,7 +1868,7 @@ int lfs_remove(lfs_t *lfs, const char *path) {
}
lfs_dir_t dir;
if ((0xf & entry.d.type) == LFS_TYPE_DIR) {
if (entry.d.type == LFS_TYPE_DIR) {
// must be empty before removal, checking size
// without masking top bit checks for any case where
// dir is not empty
@@ -1994,7 +1887,7 @@ int lfs_remove(lfs_t *lfs, const char *path) {
}
// if we were a directory, find pred, replace tail
if ((0xf & entry.d.type) == LFS_TYPE_DIR) {
if (entry.d.type == LFS_TYPE_DIR) {
int res = lfs_pred(lfs, dir.pair, &cwd);
if (res < 0) {
return res;
@@ -2004,7 +1897,7 @@ int lfs_remove(lfs_t *lfs, const char *path) {
cwd.d.tail[0] = dir.d.tail[0];
cwd.d.tail[1] = dir.d.tail[1];
err = lfs_dir_commit(lfs, &cwd, NULL);
err = lfs_dir_commit(lfs, &cwd, NULL, 0);
if (err) {
return err;
}
@@ -2057,7 +1950,7 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
}
lfs_dir_t dir;
if (prevexists && (0xf & preventry.d.type) == LFS_TYPE_DIR) {
if (prevexists && preventry.d.type == LFS_TYPE_DIR) {
// must be empty before removal, checking size
// without masking top bit checks for any case where
// dir is not empty
@@ -2070,11 +1963,8 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
}
// mark as moving
oldentry.d.type |= LFS_STRUCT_MOVED;
err = lfs_dir_update(lfs, &oldcwd, &oldentry,
&(struct lfs_region){
0, 0,
lfs_commit_mem, &oldentry.d.type, 1});
oldentry.d.type |= 0x80;
err = lfs_dir_update(lfs, &oldcwd, &oldentry, NULL);
if (err) {
return err;
}
@@ -2087,28 +1977,16 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
// move to new location
lfs_entry_t newentry = preventry;
newentry.d = oldentry.d;
newentry.d.type &= ~LFS_STRUCT_MOVED;
newentry.d.type &= ~0x80;
newentry.d.nlen = strlen(newpath);
if (prevexists) {
err = lfs_dir_update(lfs, &newcwd, &newentry,
&(struct lfs_region){
0, 0,
lfs_commit_mem, &newentry.d, sizeof(newentry.d),
&(struct lfs_region){
sizeof(newentry.d), 0,
lfs_commit_mem, newpath, newentry.d.nlen}});
err = lfs_dir_update(lfs, &newcwd, &newentry, newpath);
if (err) {
return err;
}
} else {
err = lfs_dir_append(lfs, &newcwd, &newentry,
&(struct lfs_region){
0, +sizeof(newentry.d),
lfs_commit_mem, &newentry.d, sizeof(newentry.d),
&(struct lfs_region){
0, +newentry.d.nlen,
lfs_commit_mem, newpath, newentry.d.nlen}});
err = lfs_dir_append(lfs, &newcwd, &newentry, newpath);
if (err) {
return err;
}
@@ -2126,7 +2004,7 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
}
// if we were a directory, find pred, replace tail
if (prevexists && (0xf & preventry.d.type) == LFS_TYPE_DIR) {
if (prevexists && preventry.d.type == LFS_TYPE_DIR) {
int res = lfs_pred(lfs, dir.pair, &newcwd);
if (res < 0) {
return res;
@@ -2136,7 +2014,7 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
newcwd.d.tail[0] = dir.d.tail[0];
newcwd.d.tail[1] = dir.d.tail[1];
err = lfs_dir_commit(lfs, &newcwd, NULL);
err = lfs_dir_commit(lfs, &newcwd, NULL, 0);
if (err) {
return err;
}
@@ -2227,9 +2105,9 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
// create free lookahead
memset(lfs->free.buffer, 0, lfs->cfg->lookahead/8);
lfs->free.begin = 0;
lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count);
lfs->free.off = 0;
lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count);
lfs->free.i = 0;
lfs_alloc_ack(lfs);
// create superblock dir
@@ -2246,7 +2124,7 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
return err;
}
err = lfs_dir_commit(lfs, &root, NULL);
err = lfs_dir_commit(lfs, &root, NULL, 0);
if (err) {
return err;
}
@@ -2256,7 +2134,8 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
// write superblocks
lfs_superblock_t superblock = {
.d.type = LFS_STRUCT_DIR | LFS_TYPE_SUPERBLOCK,
.off = sizeof(superdir.d),
.d.type = LFS_TYPE_SUPERBLOCK,
.d.elen = sizeof(superblock.d) - sizeof(superblock.d.magic) - 4,
.d.nlen = sizeof(superblock.d.magic),
.d.version = LFS_DISK_VERSION,
@@ -2273,9 +2152,10 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
lfs_superblock_tole32(&superblock.d);
bool valid = false;
for (int i = 0; i < 2; i++) {
err = lfs_dir_commit(lfs, &superdir, &(struct lfs_region){
sizeof(superdir.d), 0,
lfs_commit_mem, &superblock.d, sizeof(superblock.d)});
err = lfs_dir_commit(lfs, &superdir, (struct lfs_region[]){
{sizeof(superdir.d), sizeof(superblock.d),
&superblock.d, sizeof(superblock.d)}
}, 1);
if (err && err != LFS_ERR_CORRUPT) {
return err;
}
@@ -2304,9 +2184,9 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) {
}
// setup free lookahead
lfs->free.begin = 0;
lfs->free.size = 0;
lfs->free.off = 0;
lfs->free.size = 0;
lfs->free.i = 0;
lfs_alloc_ack(lfs);
// load superblock
@@ -2330,7 +2210,7 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) {
}
if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) {
LFS_ERROR("Invalid superblock at %d %d", dir.pair[0], dir.pair[1]);
LFS_ERROR("Invalid superblock at %d %d", 0, 1);
return LFS_ERR_CORRUPT;
}
@@ -2357,6 +2237,8 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
}
// iterate over metadata pairs
lfs_dir_t dir;
lfs_entry_t entry;
lfs_block_t cwd[2] = {0, 1};
while (true) {
@@ -2367,14 +2249,12 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
}
}
lfs_dir_t dir;
int err = lfs_dir_fetch(lfs, &dir, cwd);
if (err) {
return err;
}
// iterate over contents
lfs_entry_t entry;
while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size)-4) {
err = lfs_bd_read(lfs, dir.pair[0], dir.off,
&entry.d, sizeof(entry.d));
@@ -2383,8 +2263,8 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
return err;
}
dir.off += 4 + entry.d.elen + entry.d.alen + entry.d.nlen;
if ((0x70 & entry.d.type) == LFS_STRUCT_CTZ) {
dir.off += lfs_entry_size(&entry);
if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) {
err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL,
entry.d.u.file.head, entry.d.u.file.size, cb, data);
if (err) {
@@ -2474,7 +2354,7 @@ static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2],
break;
}
if (((0x70 & entry->d.type) == LFS_STRUCT_DIR) &&
if (((0x70 & entry->d.type) == (0x70 & LFS_TYPE_DIR)) &&
lfs_paircmp(entry->d.u.dir, dir) == 0) {
return true;
}
@@ -2514,7 +2394,7 @@ static int lfs_moved(lfs_t *lfs, const void *e) {
break;
}
if (!(LFS_STRUCT_MOVED & entry.d.type) &&
if (!(0x80 & entry.d.type) &&
memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) {
return true;
}
@@ -2539,10 +2419,7 @@ static int lfs_relocate(lfs_t *lfs,
entry.d.u.dir[0] = newpair[0];
entry.d.u.dir[1] = newpair[1];
int err = lfs_dir_update(lfs, &parent, &entry,
&(struct lfs_region){
0, 0,
lfs_commit_mem, &entry.d, sizeof(entry.d)});
int err = lfs_dir_update(lfs, &parent, &entry, NULL);
if (err) {
return err;
}
@@ -2569,7 +2446,7 @@ static int lfs_relocate(lfs_t *lfs,
parent.d.tail[0] = newpair[0];
parent.d.tail[1] = newpair[1];
return lfs_dir_commit(lfs, &parent, NULL);
return lfs_dir_commit(lfs, &parent, NULL, 0);
}
// couldn't find dir, must be new
@@ -2611,7 +2488,7 @@ int lfs_deorphan(lfs_t *lfs) {
pdir.d.tail[0] = cwd.d.tail[0];
pdir.d.tail[1] = cwd.d.tail[1];
err = lfs_dir_commit(lfs, &pdir, NULL);
err = lfs_dir_commit(lfs, &pdir, NULL, 0);
if (err) {
return err;
}
@@ -2627,7 +2504,7 @@ int lfs_deorphan(lfs_t *lfs) {
pdir.d.tail[0] = entry.d.u.dir[0];
pdir.d.tail[1] = entry.d.u.dir[1];
err = lfs_dir_commit(lfs, &pdir, NULL);
err = lfs_dir_commit(lfs, &pdir, NULL, 0);
if (err) {
return err;
}
@@ -2649,7 +2526,7 @@ int lfs_deorphan(lfs_t *lfs) {
}
// found moved entry
if (entry.d.type & LFS_STRUCT_MOVED) {
if (entry.d.type & 0x80) {
int moved = lfs_moved(lfs, &entry.d.u);
if (moved < 0) {
return moved;
@@ -2665,11 +2542,8 @@ int lfs_deorphan(lfs_t *lfs) {
} else {
LFS_DEBUG("Found partial move %d %d",
entry.d.u.dir[0], entry.d.u.dir[1]);
entry.d.type &= ~LFS_STRUCT_MOVED;
err = lfs_dir_update(lfs, &cwd, &entry,
&(struct lfs_region){
0, 0,
lfs_commit_mem, &entry.d, sizeof(entry.d)});
entry.d.type &= ~0x80;
err = lfs_dir_update(lfs, &cwd, &entry, NULL);
if (err) {
return err;
}

23
lfs.h
View File

@@ -74,15 +74,9 @@ enum lfs_error {
// File types
enum lfs_type {
// file type
LFS_TYPE_REG = 0x01,
LFS_TYPE_DIR = 0x02,
LFS_TYPE_SUPERBLOCK = 0x0e,
// on disk structure
LFS_STRUCT_CTZ = 0x10,
LFS_STRUCT_DIR = 0x20,
LFS_STRUCT_MOVED = 0x80,
LFS_TYPE_REG = 0x11,
LFS_TYPE_DIR = 0x22,
LFS_TYPE_SUPERBLOCK = 0x2e,
};
// File open flags
@@ -196,7 +190,6 @@ struct lfs_info {
/// littlefs data structures ///
typedef struct lfs_entry {
lfs_off_t off;
lfs_size_t size;
struct lfs_disk_entry {
uint8_t type;
@@ -250,6 +243,8 @@ typedef struct lfs_dir {
} lfs_dir_t;
typedef struct lfs_superblock {
lfs_off_t off;
struct lfs_disk_superblock {
uint8_t type;
uint8_t elen;
@@ -264,9 +259,9 @@ typedef struct lfs_superblock {
} lfs_superblock_t;
typedef struct lfs_free {
lfs_block_t begin;
lfs_block_t size;
lfs_block_t off;
lfs_block_t size;
lfs_block_t i;
lfs_block_t ack;
uint32_t *buffer;
} lfs_free_t;
@@ -325,10 +320,6 @@ int lfs_remove(lfs_t *lfs, const char *path);
// If the destination exists, it must match the source in type.
// If the destination is a directory, the directory must be empty.
//
// Note: If power loss occurs, it is possible that the file or directory
// will exist in both the oldpath and newpath simultaneously after the
// next mount.
//
// Returns a negative error code on failure.
int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath);

View File

@@ -110,10 +110,35 @@ lfs_alloc_singleproc multiprocreuse
lfs_verify multiprocreuse
lfs_verify singleprocreuse
echo "--- Cleanup ---"
lfs_remove multiprocreuse
lfs_remove singleprocreuse
echo "--- Lookahead overflow test ---"
lfs_mkdir overflow
for name in bacon eggs pancakes
do
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
// // setup lookahead to almost overflow
// lfs.free.begin = ((lfs_size_t)-1) - 2*$SIZE;
// lfs.free.size = 0;
// lfs.free.off = 0;
lfs_file_open(&lfs, &file[0], "overflow/$name",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_APPEND) => 0;
size = strlen("$name");
memcpy(buffer, "$name", size);
for (int i = 0; i < $SIZE; i++) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
lfs_unmount(&lfs) => 0;
TEST
done
lfs_verify overflow
lfs_remove overflow
echo "--- Exhaustion test ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
@@ -289,7 +314,7 @@ tests/test.py << TEST
}
lfs_file_close(&lfs, &file[0]) => 0;
// open whole
// open hole
lfs_remove(&lfs, "bump") => 0;
lfs_mkdir(&lfs, "splitdir") => 0;
@@ -301,5 +326,130 @@ tests/test.py << TEST
lfs_unmount(&lfs) => 0;
TEST
echo "--- Outdated lookahead test ---"
rm -rf blocks
tests/test.py << TEST
lfs_format(&lfs, &cfg) => 0;
lfs_mount(&lfs, &cfg) => 0;
// fill completely with two files
lfs_file_open(&lfs, &file[0], "exhaustion1",
LFS_O_WRONLY | LFS_O_CREAT) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0;
i < ((cfg.block_count-4)/2)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
lfs_file_open(&lfs, &file[0], "exhaustion2",
LFS_O_WRONLY | LFS_O_CREAT) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0;
i < ((cfg.block_count-4+1)/2)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
// remount to force reset of lookahead
lfs_unmount(&lfs) => 0;
lfs_mount(&lfs, &cfg) => 0;
// rewrite one file
lfs_file_open(&lfs, &file[0], "exhaustion1",
LFS_O_WRONLY | LFS_O_TRUNC) => 0;
lfs_file_sync(&lfs, &file[0]) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0;
i < ((cfg.block_count-4)/2)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
// rewrite second file, this requires lookahead does not
// use old population
lfs_file_open(&lfs, &file[0], "exhaustion2",
LFS_O_WRONLY | LFS_O_TRUNC) => 0;
lfs_file_sync(&lfs, &file[0]) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0;
i < ((cfg.block_count-4+1)/2)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
TEST
echo "--- Outdated lookahead and split dir test ---"
rm -rf blocks
tests/test.py << TEST
lfs_format(&lfs, &cfg) => 0;
lfs_mount(&lfs, &cfg) => 0;
// fill completely with two files
lfs_file_open(&lfs, &file[0], "exhaustion1",
LFS_O_WRONLY | LFS_O_CREAT) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0;
i < ((cfg.block_count-4)/2)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
lfs_file_open(&lfs, &file[0], "exhaustion2",
LFS_O_WRONLY | LFS_O_CREAT) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0;
i < ((cfg.block_count-4+1)/2)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
// remount to force reset of lookahead
lfs_unmount(&lfs) => 0;
lfs_mount(&lfs, &cfg) => 0;
// rewrite one file with a hole of one block
lfs_file_open(&lfs, &file[0], "exhaustion1",
LFS_O_WRONLY | LFS_O_TRUNC) => 0;
lfs_file_sync(&lfs, &file[0]) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0;
i < ((cfg.block_count-4)/2 - 1)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
// try to allocate a directory, should fail!
lfs_mkdir(&lfs, "split") => LFS_ERR_NOSPC;
// file should not fail
lfs_file_open(&lfs, &file[0], "notasplit",
LFS_O_WRONLY | LFS_O_CREAT) => 0;
lfs_file_write(&lfs, &file[0], "hi", 2) => 2;
lfs_file_close(&lfs, &file[0]) => 0;
lfs_unmount(&lfs) => 0;
TEST
echo "--- Results ---"
tests/stats.py

View File

@@ -1,13 +1,13 @@
#!/bin/bash
set -eu
echo "=== Parallel tests ==="
echo "=== Interspersed tests ==="
rm -rf blocks
tests/test.py << TEST
lfs_format(&lfs, &cfg) => 0;
TEST
echo "--- Parallel file test ---"
echo "--- Interspersed file test ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_file_open(&lfs, &file[0], "a", LFS_O_WRONLY | LFS_O_CREAT) => 0;
@@ -77,7 +77,7 @@ tests/test.py << TEST
lfs_unmount(&lfs) => 0;
TEST
echo "--- Parallel remove file test ---"
echo "--- Interspersed remove file test ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_file_open(&lfs, &file[0], "e", LFS_O_WRONLY | LFS_O_CREAT) => 0;