Compare commits

..

27 Commits

Author SHA1 Message Date
Christopher Haster
2ad435ed63 Added files test to littlefs-fuse tests in Travis 2018-01-20 19:22:38 -06:00
Christopher Haster
1fb6a19520 Reduced ctz traverse runtime by 2x
Unfortunately for us, the ctz skip-list does not offer very much benefit
for full traversals. Since the information about which blocks are in
use are spread throughout the file, we can't use the fast-lanes
embedded in the skip-list without missing blocks.

However, it turns out we can at least use the 2nd level of the skip-list
without missing any blocks. From an asymptotic analysis, a constant speed
up isn't interesting, but from a pragmatic perspective, a 2x speedup is
not bad.
2018-01-12 12:07:45 -06:00
Christopher Haster
db8872781a Added error code LFS_ERR_NOTEMPTY
As noted by itayzafrir, removing a non-empty directory should
error with ENOTEMPTY, not EINVAL
2018-01-12 12:07:40 -06:00
Christopher Haster
c2fab8fabb Added asserts on geometry and updated config documentation
littlefs had an unwritten assumption that the block device's program
size would be a multiple of the read size, and the block size would
be a multiple of the program size. This has already caused confusion
for users. Added a note and assert to catch unexpected geometries
early.

Also found that the prog/erase functions indicated they must return
LFS_ERR_CORRUPT to catch bad blocks. This is no longer true as errors
are found by CRC.
2018-01-11 11:56:09 -06:00
Christopher Haster
472ccc4203 Fixed file truncation without writes
In the open call, the LFS_O_TRUNC flag was correctly zeroing the file, but
it wasn't actually writing the change out to disk. This went unnoticed because
in the cases where the truncate was followed by a file write, the
updated contents would be written out correctly.

Marking the file as dirty if the file isn't already truncated fixes the
problem with the least impact. Also added better test cases around
truncating files.
2018-01-11 10:26:33 -06:00
Christopher Haster
aea3d3db46 Fixed positive seek bounds checking
This bug was a result of an annoying corner case around intermingling
signed and unsigned offsets. The boundary check that prevents seeking
a file to a position before the file was preventing valid seeks with
positive offsets.

This corner case is a bit more complicated than it looks because the
offset is signed, while the size of the file is unsigned. Simply
casting both to signed or unsigned offsets won't handle large files.
2018-01-03 15:00:04 -06:00
Christopher Haster
be22d3449f Updated links to Mbed OS 2017-12-27 12:59:54 -06:00
Christopher Haster
425aa3c694 Fixed issue with immediate exhaustion and small unaligned storage
This was a small hole in the logic that handles initializing the
lookahead buffer. To imitate exhaustion (so the block allocator
will trigger a scan), the lookahead buffer is rewound a full
lookahead and set up to look like it is exhausted. However,
unlike normal allocation, this rewind was not kept aligned to
a multiple of the scan size, which is limited by both the
lookahead buffer and the total storage size.

This bug went unnoticed for so long because it only causes
problems when the block device is both:
1. Not aligned to the lookahead buffer (not a power of 2)
2. Smaller than the lookahead buffer

While this seems like a strange corner case for a block device,
this turned out to be very common for internal flash, especially
when a handleful of blocks are reserved for code.
2017-12-27 12:59:32 -06:00
Christopher Haster
5ee20e8d77 Fixed pipefail issue that was preventing CI from reporting errors 2017-11-22 14:49:48 -06:00
Christopher Haster
bf78b09d37 Added directory list for synchronizing in flight directories
As it was, if a user operated on a directory while at the same
time iterating over the directory, the directory objects could
fall out of sync. In the best case, files may be skipped while
removing everything in a file, in the worst case, a very poorly
timed directory relocate could be missed.

Simple fix is to add the same directory tracking that is currently
in use for files, at a small code+complexity cost.
2017-11-22 14:49:43 -06:00
Christopher Haster
e169d06c57 Removed vestigial function declaration 2017-11-21 22:01:20 -06:00
Christopher Haster
996cd8af22 Revisited documentation
Mostly changed the wording around the goals/features of littlefs based
on feedback from other developers.

Also added in project links now that there are a few of those floating
around. And made the README a bit easier to navigate.
2017-11-20 00:08:02 -06:00
Christopher Haster
78c79ecb9e Added QUIET flag to tests so CI is readable 2017-11-16 17:54:44 -06:00
Christopher Haster
f9f4f5ccec Fixed standard name mismatch LFS_ERR_EXISTS -> LFS_ERR_EXIST
Matches the standard EEXIST name found on most systems. Other than
this name, all other common constant names were consistent in this
manner.
2017-11-16 17:50:14 -06:00
Christopher Haster
843e3c6c75 Added sticky-bit for preventing file syncs after write errors
Short story, files are no longer committed to directories during
file sync/close if the last write did not complete successfully.
This avoids a set of interesting user-experience issues related
to the end-of-life behaviour of the filesystem.

As a filesystem approaches end-of-life, the chances of running into
LFS_ERR_NOSPC grows rather quickly. Since this condition occurs after
at the end of a devices life, it's likely that operating in these
conditions hasn't been tested thoroughly.

In the specific case of file-writes, you can hit an LFS_ERR_NOSPC after
parts of the file have been written out. If the program simply continues
and closes the file, the file is written out half completed. Since
littlefs has a strong garuntee the prevents half-writes, it's unlikely
this state of the file would be expected.

To make things worse, since close is also responsible for memory
cleanup, it's actually _impossible_ to continue working as it was
without leaking memory.

By prevent the file commits, end-of-life behaviour should at least retain
a previous copy of the filesystem without any surprises.
2017-11-16 17:25:41 -06:00
Christopher Haster
2612e1b3fa Modified lfs_ctz_extend to be a little bit safer
Specifically around error handling. As is, incorrectly handled
errors could cause higher code to get uninitialized blocks,
potentially leading to writes to arbitray blocks on storage.
2017-11-16 15:10:17 -06:00
Christopher Haster
6664723e18 Fixed issue with committing directories to bad-blocks that are stuck
This is only an issue in the weird case that are worn down block is
left in the odd state of not being able to change the data that resides
on the block. That being said, this does pop up often when simulating
wear on block devices.

Currently, directory commits checked if the write succeeded by crcing the
block to avoid the additional RAM cost for another buffer. However,
before this commit, directory commits just checked if the block crc was
valid, rather than comparing to the expected crc. This would usually
work, unless the block was stuck in a state with valid crc.

The fix is to simply compare with the expected crc to find errors.
2017-11-16 14:53:45 -06:00
Christopher Haster
3f31c8cba3 Fixed corner case with immediate exhaustion and lookahead==block_count
The previous math for determining if we scanned all of disk wasn't set
up correctly in the lfs_mount function. If lookahead == block_count
the lfs_alloc function would think we had already searched the entire
disk.

This is only an issue if we manage to exhaust a block on the first
pass after mount, since lfs_alloc_ack resets the lookahead region
into a valid state after a succesful block allocation.
2017-11-10 10:53:33 -06:00
Christopher Haster
f4aeb8331a Fixed issue with aggressively rounding down lookahead configuration
The littlefs allows buffers to be passed statically in the case
that a system does not have a heap. Unfortunately, this means we
can't round up in the case of an unaligned lookahead buffer.

Double unfortunately, rounding down after clamping to the block device
size could result in a lookahead of zero for block devices < 32 blocks
large.

The assert in littlefs does catch this case, but rounding down prevents
support for < 32 block devices.

The solution is to simply require a 32-bit aligned buffer with an
assert. This avoids runtime problems while allowing a user to pass
in the correct buffer for < 32 block devices. Rounding up can be
handled at higher API levels.
2017-11-10 10:53:30 -06:00
Christopher Haster
db51a395ba Removed stray newline in LFS_ERROR for version 2017-11-09 19:44:23 -06:00
Christopher Haster
2ab150cc50 Removed toolchain specific warnings
- Comparisons with differently signed integer types
- Incorrectly signed constant
- Unreachable default returns
- Leaked uninitialized variables in relocate goto statements
2017-10-30 16:55:45 -05:00
Christopher Haster
0825d34f3d Adopted alternative implementation for lfs_ctz_index
Same runtime cost, however reduces the logic and avoids one of
the two big branches. See the DESIGN.md for more info.

Now uses these equations instead of the messy guess and correct method:
n = (N - w/8(popcount(N/(B-2w/8)) + 2)) / (B-2w/8)
off = N - (B-w2/8)n - w/8popcount(n)
2017-10-30 12:03:33 -05:00
Christopher Haster
46e22b2a38 Adopted lfs_ctz_index implementation using popcount
This reduces the O(n^2logn) runtime to read a file to only O(nlog).
The extra O(n) did not touch the disk, so it isn't a problem until the
files become very large, but this solution comes with very little cost.

Long story short, you can find the block index + offset pair for a
CTZ linked-list with this series of formulas:

n' = floor(N / (B - 2w/8))
N' = (B - 2w/8)n' + (w/8)popcount(n')
off' = N - N'
n, off =
  n'-1, off'+B                if off' <  0
  n',   off'+(w/8)(ctz(n')+1) if off' >= 0

For the long story, you will need to see the updated DESIGN.md
2017-10-18 00:44:30 -05:00
Christopher Haster
4fdca15a0d Slight name change with ctz skip-list functions
changed:
lfs_index -> lfs_ctz_index
lfs_index_find -> lfs_ctz_find
lfs_index_append -> lfs_ctz_append
lfs_index_traverse -> lfs_ctz_traverse
2017-10-18 00:41:43 -05:00
Christopher Haster
454b588f73 Updated SPEC.md and DESIGN.md based on recent changes
- Added math behind CTZ limits
- Added documentation over atomic moves
2017-10-12 20:31:33 -05:00
Christopher Haster
f3578e3250 Removed clamping to block size in ctz linked-list
Initially, I was concerned that the number of pointers in the ctz
linked-list could exceed the storage in a block. Long story short
this isn't really possible outside of extremely small block sizes.

Since clamping impacts the layout of files on disk, removing the
block size removed quite a bit of logic and corner cases. Replaced
with an assert on block size during initialization.

---

Long story long, the minimum block size needed to store all ctz
pointers in a filesystem can be found with this formula:

B = (w/8)*log2(2^w / (B - 2*(w/8)))

where:
B = block size in bytes
w = pointer width in bits

It's not a very pretty formula, but does give us some useful info
if we apply some math:

min block size:
32 bit ctz linked-list = 104 bytes
64 bit ctz linked-list = 448 bytes

For littlefs, 128 bytes is a perfectly reasonable minimum block size.
2017-10-12 20:31:33 -05:00
Christopher Haster
83d4c614a0 Updated copyright
Due to employee contract
Per ARM license remains under Apache 2.0
2017-10-12 20:29:10 -05:00
16 changed files with 919 additions and 433 deletions

View File

@@ -9,13 +9,12 @@ script:
-include stdio.h -Werror' make all size
# run tests
- make test
- make test QUIET=1
# run tests with a few different configurations
- CFLAGS="-DLFS_READ_SIZE=1 -DLFS_PROG_SIZE=1" make test
- CFLAGS="-DLFS_READ_SIZE=512 -DLFS_PROG_SIZE=512" make test
- CFLAGS="-DLFS_BLOCK_COUNT=1023" make test
- CFLAGS="-DLFS_LOOKAHEAD=2047" make test
- CFLAGS="-DLFS_READ_SIZE=1 -DLFS_PROG_SIZE=1" make test QUIET=1
- CFLAGS="-DLFS_READ_SIZE=512 -DLFS_PROG_SIZE=512" make test QUIET=1
- CFLAGS="-DLFS_BLOCK_COUNT=1023 -DLFS_LOOKAHEAD=2048" make test QUIET=1
# self-host with littlefs-fuse for fuzz test
- make -C littlefs-fuse
@@ -28,7 +27,7 @@ script:
- cp -r $(git ls-tree --name-only HEAD) mount/littlefs
- cd mount/littlefs
- ls
- make -B test_dirs
- make -B test_dirs test_files QUIET=1
before_install:
- fusermount -V

413
DESIGN.md
View File

@@ -1,6 +1,6 @@
## The design of the little filesystem
The littlefs is a little fail-safe filesystem designed for embedded systems.
A little fail-safe filesystem designed for embedded systems.
```
| | | .---._____
@@ -16,9 +16,9 @@ more about filesystem design by tackling the relative unsolved problem of
managing a robust filesystem resilient to power loss on devices
with limited RAM and ROM.
The embedded systems the littlefs is targeting are usually 32bit
microcontrollers with around 32Kbytes of RAM and 512Kbytes of ROM. These are
often paired with SPI NOR flash chips with about 4Mbytes of flash storage.
The embedded systems the littlefs is targeting are usually 32 bit
microcontrollers with around 32KB of RAM and 512KB of ROM. These are
often paired with SPI NOR flash chips with about 4MB of flash storage.
Flash itself is a very interesting piece of technology with quite a bit of
nuance. Unlike most other forms of storage, writing to flash requires two
@@ -32,17 +32,17 @@ has more information if you are interesting in how this works.
This leaves us with an interesting set of limitations that can be simplified
to three strong requirements:
1. **Fail-safe** - This is actually the main goal of the littlefs and the focus
of this project. Embedded systems are usually designed without a shutdown
routine and a notable lack of user interface for recovery, so filesystems
targeting embedded systems should be prepared to lose power an any given
time.
1. **Power-loss resilient** - This is the main goal of the littlefs and the
focus of this project. Embedded systems are usually designed without a
shutdown routine and a notable lack of user interface for recovery, so
filesystems targeting embedded systems must be prepared to lose power an
any given time.
Despite this state of things, there are very few embedded filesystems that
handle power loss in a reasonable manner, and can become corrupted if the
user is unlucky enough.
handle power loss in a reasonable manner, and most can become corrupted if
the user is unlucky enough.
2. **Wear awareness** - Due to the destructive nature of flash, most flash
2. **Wear leveling** - Due to the destructive nature of flash, most flash
chips have a limited number of erase cycles, usually in the order of around
100,000 erases per block for NOR flash. Filesystems that don't take wear
into account can easily burn through blocks used to store frequently updated
@@ -78,9 +78,9 @@ summary of the general ideas behind some of them.
Most of the existing filesystems fall into the one big category of filesystem
designed in the early days of spinny magnet disks. While there is a vast amount
of interesting technology and ideas in this area, the nature of spinny magnet
disks encourage properties such as grouping writes near each other, that don't
disks encourage properties, such as grouping writes near each other, that don't
make as much sense on recent storage types. For instance, on flash, write
locality is not as important and can actually increase wear destructively.
locality is not important and can actually increase wear destructively.
One of the most popular designs for flash filesystems is called the
[logging filesystem](https://en.wikipedia.org/wiki/Log-structured_file_system).
@@ -97,8 +97,7 @@ scaling as the size of storage increases. And most filesystems compensate by
caching large parts of the filesystem in RAM, a strategy that is unavailable
for embedded systems.
Another interesting filesystem design technique that the littlefs borrows the
most from, is the [copy-on-write (COW)](https://en.wikipedia.org/wiki/Copy-on-write).
Another interesting filesystem design technique is that of [copy-on-write (COW)](https://en.wikipedia.org/wiki/Copy-on-write).
A good example of this is the [btrfs](https://en.wikipedia.org/wiki/Btrfs)
filesystem. COW filesystems can easily recover from corrupted blocks and have
natural protection against power loss. However, if they are not designed with
@@ -150,12 +149,12 @@ check our checksum we notice that block 1 was corrupted. So we fall back to
block 2 and use the value 9.
Using this concept, the littlefs is able to update metadata blocks atomically.
There are a few other tweaks, such as using a 32bit crc and using sequence
There are a few other tweaks, such as using a 32 bit crc and using sequence
arithmetic to handle revision count overflow, but the basic concept
is the same. These metadata pairs define the backbone of the littlefs, and the
rest of the filesystem is built on top of these atomic updates.
## Files
## Non-meta data
Now, the metadata pairs do come with some drawbacks. Most notably, each pair
requires two blocks for each block of data. I'm sure users would be very
@@ -200,7 +199,7 @@ Now we could just leave files here, copying the entire file on write
provides the synchronization without the duplicated memory requirements
of the metadata blocks. However, we can do a bit better.
## CTZ linked-lists
## CTZ skip-lists
There are many different data structures for representing the actual
files in filesystems. Of these, the littlefs uses a rather unique [COW](https://upload.wikimedia.org/wikipedia/commons/0/0c/Cow_female_black_white.jpg)
@@ -224,12 +223,12 @@ Exhibit A: A linked-list
To get around this, the littlefs, at its heart, stores files backwards. Each
block points to its predecessor, with the first block containing no pointers.
If you think about this, it makes a bit of sense. Appending blocks just point
to their predecessor and no other blocks need to be updated. If we update
a block in the middle, we will need to copy out the blocks that follow,
but can reuse the blocks before the modified block. Since most file operations
either reset the file each write or append to files, this design avoids
copying the file in the most common cases.
If you think about for a while, it starts to make a bit of sense. Appending
blocks just point to their predecessor and no other blocks need to be updated.
If we update a block in the middle, we will need to copy out the blocks that
follow, but can reuse the blocks before the modified block. Since most file
operations either reset the file each write or append to files, this design
avoids copying the file in the most common cases.
```
Exhibit B: A backwards linked-list
@@ -246,19 +245,19 @@ runtime to just _read_ a file? That's awful. Keep in mind reading files are
usually the most common filesystem operation.
To avoid this problem, the littlefs uses a multilayered linked-list. For
every block that is divisible by a power of two, the block contains an
additional pointer that points back by that power of two. Another way of
thinking about this design is that there are actually many linked-lists
threaded together, with each linked-lists skipping an increasing number
of blocks. If you're familiar with data-structures, you may have also
recognized that this is a deterministic skip-list.
every nth block where n is divisible by 2^x, the block contains a pointer
to block n-2^x. So each block contains anywhere from 1 to log2(n) pointers
that skip to various sections of the preceding list. If you're familiar with
data-structures, you may have recognized that this is a type of deterministic
skip-list.
To find the power of two factors efficiently, we can use the instruction
[count trailing zeros (CTZ)](https://en.wikipedia.org/wiki/Count_trailing_zeros),
which is where this linked-list's name comes from.
The name comes from the use of the
[count trailing zeros (CTZ)](https://en.wikipedia.org/wiki/Count_trailing_zeros)
instruction, which allows us to calculate the power-of-two factors efficiently.
For a given block n, the block contains ctz(n)+1 pointers.
```
Exhibit C: A backwards CTZ linked-list
Exhibit C: A backwards CTZ skip-list
.--------. .--------. .--------. .--------. .--------. .--------.
| data 0 |<-| data 1 |<-| data 2 |<-| data 3 |<-| data 4 |<-| data 5 |
| |<-| |--| |<-| |--| | | |
@@ -266,6 +265,9 @@ Exhibit C: A backwards CTZ linked-list
'--------' '--------' '--------' '--------' '--------' '--------'
```
The additional pointers allow us to navigate the data-structure on disk
much more efficiently than in a single linked-list.
Taking exhibit C for example, here is the path from data block 5 to data
block 1. You can see how data block 3 was completely skipped:
```
@@ -285,15 +287,133 @@ The path to data block 0 is even more quick, requiring only two jumps:
'--------' '--------' '--------' '--------' '--------' '--------'
```
The CTZ linked-list has quite a few interesting properties. All of the pointers
in the block can be found by just knowing the index in the list of the current
block, and, with a bit of math, the amortized overhead for the linked-list is
only two pointers per block. Most importantly, the CTZ linked-list has a
worst case lookup runtime of O(logn), which brings the runtime of reading a
file down to O(n logn). Given that the constant runtime is divided by the
amount of data we can store in a block, this is pretty reasonable.
We can find the runtime complexity by looking at the path to any block from
the block containing the most pointers. Every step along the path divides
the search space for the block in half. This gives us a runtime of O(logn).
To get to the block with the most pointers, we can perform the same steps
backwards, which puts the runtime at O(2logn) = O(logn). The interesting
part about this data structure is that this optimal path occurs naturally
if we greedily choose the pointer that covers the most distance without passing
our target block.
Here is what it might look like to update a file stored with a CTZ linked-list:
So now we have a representation of files that can be appended trivially with
a runtime of O(1), and can be read with a worst case runtime of O(nlogn).
Given that the the runtime is also divided by the amount of data we can store
in a block, this is pretty reasonable.
Unfortunately, the CTZ skip-list comes with a few questions that aren't
straightforward to answer. What is the overhead? How do we handle more
pointers than we can store in a block? How do we store the skip-list in
a directory entry?
One way to find the overhead per block is to look at the data structure as
multiple layers of linked-lists. Each linked-list skips twice as many blocks
as the previous linked-list. Another way of looking at it is that each
linked-list uses half as much storage per block as the previous linked-list.
As we approach infinity, the number of pointers per block forms a geometric
series. Solving this geometric series gives us an average of only 2 pointers
per block.
![overhead_per_block](https://latex.codecogs.com/svg.latex?%5Clim_%7Bn%5Cto%5Cinfty%7D%5Cfrac%7B1%7D%7Bn%7D%5Csum_%7Bi%3D0%7D%5E%7Bn%7D%5Cleft%28%5Ctext%7Bctz%7D%28i%29&plus;1%5Cright%29%20%3D%20%5Csum_%7Bi%3D0%7D%5Cfrac%7B1%7D%7B2%5Ei%7D%20%3D%202)
Finding the maximum number of pointers in a block is a bit more complicated,
but since our file size is limited by the integer width we use to store the
size, we can solve for it. Setting the overhead of the maximum pointers equal
to the block size we get the following equation. Note that a smaller block size
results in more pointers, and a larger word width results in larger pointers.
![maximum overhead](https://latex.codecogs.com/svg.latex?B%20%3D%20%5Cfrac%7Bw%7D%7B8%7D%5Cleft%5Clceil%5Clog_2%5Cleft%28%5Cfrac%7B2%5Ew%7D%7BB-2%5Cfrac%7Bw%7D%7B8%7D%7D%5Cright%29%5Cright%5Crceil)
where:
B = block size in bytes
w = word width in bits
Solving the equation for B gives us the minimum block size for various word
widths:
32 bit CTZ skip-list = minimum block size of 104 bytes
64 bit CTZ skip-list = minimum block size of 448 bytes
Since littlefs uses a 32 bit word size, we are limited to a minimum block
size of 104 bytes. This is a perfectly reasonable minimum block size, with most
block sizes starting around 512 bytes. So we can avoid additional logic to
avoid overflowing our block's capacity in the CTZ skip-list.
So, how do we store the skip-list in a directory entry? A naive approach would
be to store a pointer to the head of the skip-list, the length of the file
in bytes, the index of the head block in the skip-list, and the offset in the
head block in bytes. However this is a lot of information, and we can observe
that a file size maps to only one block index + offset pair. So it should be
sufficient to store only the pointer and file size.
But there is one problem, calculating the block index + offset pair from a
file size doesn't have an obvious implementation.
We can start by just writing down an equation. The first idea that comes to
mind is to just use a for loop to sum together blocks until we reach our
file size. We can write this equation as a summation:
![summation1](https://latex.codecogs.com/svg.latex?N%20%3D%20%5Csum_i%5En%5Cleft%5BB-%5Cfrac%7Bw%7D%7B8%7D%5Cleft%28%5Ctext%7Bctz%7D%28i%29&plus;1%5Cright%29%5Cright%5D)
where:
B = block size in bytes
w = word width in bits
n = block index in skip-list
N = file size in bytes
And this works quite well, but is not trivial to calculate. This equation
requires O(n) to compute, which brings the entire runtime of reading a file
to O(n^2logn). Fortunately, the additional O(n) does not need to touch disk,
so it is not completely unreasonable. But if we could solve this equation into
a form that is easily computable, we can avoid a big slowdown.
Unfortunately, the summation of the CTZ instruction presents a big challenge.
How would you even begin to reason about integrating a bitwise instruction?
Fortunately, there is a powerful tool I've found useful in these situations:
The [On-Line Encyclopedia of Integer Sequences (OEIS)](https://oeis.org/).
If we work out the first couple of values in our summation, we find that CTZ
maps to [A001511](https://oeis.org/A001511), and its partial summation maps
to [A005187](https://oeis.org/A005187), and surprisingly, both of these
sequences have relatively trivial equations! This leads us to a rather
unintuitive property:
![mindblown](https://latex.codecogs.com/svg.latex?%5Csum_i%5En%5Cleft%28%5Ctext%7Bctz%7D%28i%29&plus;1%5Cright%29%20%3D%202n-%5Ctext%7Bpopcount%7D%28n%29)
where:
ctz(i) = the number of trailing bits that are 0 in i
popcount(i) = the number of bits that are 1 in i
It's a bit bewildering that these two seemingly unrelated bitwise instructions
are related by this property. But if we start to disect this equation we can
see that it does hold. As n approaches infinity, we do end up with an average
overhead of 2 pointers as we find earlier. And popcount seems to handle the
error from this average as it accumulates in the CTZ skip-list.
Now we can substitute into the original equation to get a trivial equation
for a file size:
![summation2](https://latex.codecogs.com/svg.latex?N%20%3D%20Bn%20-%20%5Cfrac%7Bw%7D%7B8%7D%5Cleft%282n-%5Ctext%7Bpopcount%7D%28n%29%5Cright%29)
Unfortunately, we're not quite done. The popcount function is non-injective,
so we can only find the file size from the block index, not the other way
around. However, we can solve for an n' block index that is greater than n
with an error bounded by the range of the popcount function. We can then
repeatedly substitute this n' into the original equation until the error
is smaller than the integer division. As it turns out, we only need to
perform this substitution once. Now we directly calculate our block index:
![formulaforn](https://latex.codecogs.com/svg.latex?n%20%3D%20%5Cleft%5Clfloor%5Cfrac%7BN-%5Cfrac%7Bw%7D%7B8%7D%5Cleft%28%5Ctext%7Bpopcount%7D%5Cleft%28%5Cfrac%7BN%7D%7BB-2%5Cfrac%7Bw%7D%7B8%7D%7D-1%5Cright%29&plus;2%5Cright%29%7D%7BB-2%5Cfrac%7Bw%7D%7B8%7D%7D%5Cright%5Crfloor)
Now that we have our block index n, we can just plug it back into the above
equation to find the offset. However, we do need to rearrange the equation
a bit to avoid integer overflow:
![formulaforoff](https://latex.codecogs.com/svg.latex?%5Cmathit%7Boff%7D%20%3D%20N%20-%20%5Cleft%28B-2%5Cfrac%7Bw%7D%7B8%7D%5Cright%29n%20-%20%5Cfrac%7Bw%7D%7B8%7D%5Ctext%7Bpopcount%7D%28n%29)
The solution involves quite a bit of math, but computers are very good at math.
We can now solve for the block index + offset while only needed to store the
file size in O(1).
Here is what it might look like to update a file stored with a CTZ skip-list:
```
block 1 block 2
.---------.---------.
@@ -367,7 +487,7 @@ v
## Block allocation
So those two ideas provide the grounds for the filesystem. The metadata pairs
give us directories, and the CTZ linked-lists give us files. But this leaves
give us directories, and the CTZ skip-lists give us files. But this leaves
one big [elephant](https://upload.wikimedia.org/wikipedia/commons/3/37/African_Bush_Elephant.jpg)
of a question. How do we get those blocks in the first place?
@@ -653,9 +773,17 @@ deorphan step that simply iterates through every directory in the linked-list
and checks it against every directory entry in the filesystem to see if it
has a parent. The deorphan step occurs on the first block allocation after
boot, so orphans should never cause the littlefs to run out of storage
prematurely.
prematurely. Note that the deorphan step never needs to run in a readonly
filesystem.
And for my final trick, moving a directory:
## The move problem
Now we have a real problem. How do we move things between directories while
remaining power resilient? Even looking at the problem from a high level,
it seems impossible. We can update directory blocks atomically, but atomically
updating two independent directory blocks is not an atomic operation.
Here's the steps the filesystem may go through to move a directory:
```
.--------.
|root dir|-.
@@ -716,18 +844,135 @@ v
'--------'
```
Note that once again we don't care about the ordering of directories in the
linked-list, so we can simply leave directories in their old positions. This
does make the diagrams a bit hard to draw, but the littlefs doesn't really
care.
We can leave any orphans up to the deorphan step to collect, but that doesn't
help the case where dir A has both dir B and the root dir as parents if we
lose power inconveniently.
It's also worth noting that once again we have an operation that isn't actually
atomic. After we add directory A to directory B, we could lose power, leaving
directory A as a part of both the root directory and directory B. However,
there isn't anything inherent to the littlefs that prevents a directory from
having multiple parents, so in this case, we just allow that to happen. Extra
care is taken to only remove a directory from the linked-list if there are
no parents left in the filesystem.
Initially, you might think this is fine. Dir A _might_ end up with two parents,
but the filesystem will still work as intended. But then this raises the
question of what do we do when the dir A wears out? For other directory blocks
we can update the parent pointer, but for a dir with two parents we would need
work out how to update both parents. And the check for multiple parents would
need to be carried out for every directory, even if the directory has never
been moved.
It also presents a bad user-experience, since the condition of ending up with
two parents is rare, it's unlikely user-level code will be prepared. Just think
about how a user would recover from a multi-parented directory. They can't just
remove one directory, since remove would report the directory as "not empty".
Other atomic filesystems simple COW the entire directory tree. But this
introduces a significant bit of complexity, which leads to code size, along
with a surprisingly expensive runtime cost during what most users assume is
a single pointer update.
Another option is to update the directory block we're moving from to point
to the destination with a sort of predicate that we have moved if the
destination exists. Unfortunately, the omnipresent concern of wear could
cause any of these directory entries to change blocks, and changing the
entry size before a move introduces complications if it spills out of
the current directory block.
So how do we go about moving a directory atomically?
We rely on the improbableness of power loss.
Power loss during a move is certainly possible, but it's actually relatively
rare. Unless a device is writing to a filesystem constantly, it's unlikely that
a power loss will occur during filesystem activity. We still need to handle
the condition, but runtime during a power loss takes a back seat to the runtime
during normal operations.
So what littlefs does is unelegantly simple. When littlefs moves a file, it
marks the file as "moving". This is stored as a single bit in the directory
entry and doesn't take up much space. Then littlefs moves the directory,
finishing with the complete remove of the "moving" directory entry.
```
.--------.
|root dir|-.
| pair 0 | |
.--------| |-'
| '--------'
| .-' '-.
| v v
| .--------. .--------.
'->| dir A |->| dir B |
| pair 0 | | pair 0 |
| | | |
'--------' '--------'
| update root directory to mark directory A as moving
v
.----------.
|root dir |-.
| pair 0 | |
.-------| moving A!|-'
| '----------'
| .-' '-.
| v v
| .--------. .--------.
'->| dir A |->| dir B |
| pair 0 | | pair 0 |
| | | |
'--------' '--------'
| update directory B to point to directory A
v
.----------.
|root dir |-.
| pair 0 | |
.-------| moving A!|-'
| '----------'
| .-----' '-.
| | v
| | .--------.
| | .->| dir B |
| | | | pair 0 |
| | | | |
| | | '--------'
| | .-------'
| v v |
| .--------. |
'->| dir A |-'
| pair 0 |
| |
'--------'
| update root to no longer contain directory A
v
.--------.
|root dir|-.
| pair 0 | |
.----| |-'
| '--------'
| |
| v
| .--------.
| .->| dir B |
| | | pair 0 |
| '--| |-.
| '--------' |
| | |
| v |
| .--------. |
'--->| dir A |-'
| pair 0 |
| |
'--------'
```
Now, if we run into a directory entry that has been marked as "moved", one
of two things is possible. Either the directory entry exists elsewhere in the
filesystem, or it doesn't. This is a O(n) operation, but only occurs in the
unlikely case we lost power during a move.
And we can easily fix the "moved" directory entry. Since we're already scanning
the filesystem during the deorphan step, we can also check for moved entries.
If we find one, we either remove the "moved" marking or remove the whole entry
if it exists elsewhere in the filesystem.
## Wear awareness
@@ -908,21 +1153,26 @@ develops errors and needs to be moved.
The second concern for the littlefs, is that blocks in the filesystem may wear
unevenly. In this situation, a filesystem may meet an early demise where
there are no more non-corrupted blocks that aren't in use. It may be entirely
possible that files were written once and left unmodified, wasting the
potential erase cycles of the blocks it sits on.
there are no more non-corrupted blocks that aren't in use. It's common to
have files that were written once and left unmodified, wasting the potential
erase cycles of the blocks it sits on.
Wear leveling is a term that describes distributing block writes evenly to
avoid the early termination of a flash part. There are typically two levels
of wear leveling:
1. Dynamic wear leveling - Blocks are distributed evenly during blocks writes.
Note that the issue with write-once files still exists in this case.
2. Static wear leveling - Unmodified blocks are evicted for new block writes.
This provides the longest lifetime for a flash device.
1. Dynamic wear leveling - Wear is distributed evenly across all **dynamic**
blocks. Usually this is accomplished by simply choosing the unused block
with the lowest amount of wear. Note this does not solve the problem of
static data.
2. Static wear leveling - Wear is distributed evenly across all **dynamic**
and **static** blocks. Unmodified blocks may be evicted for new block
writes. This does handle the problem of static data but may lead to
wear amplification.
Now, it's possible to use the revision count on metadata pairs to approximate
the wear of a metadata block. And combined with the COW nature of files, the
littlefs could provide a form of dynamic wear leveling.
In littlefs's case, it's possible to use the revision count on metadata pairs
to approximate the wear of a metadata block. And combined with the COW nature
of files, littlefs could provide your usually implementation of dynamic wear
leveling.
However, the littlefs does not. This is for a few reasons. Most notably, even
if the littlefs did implement dynamic wear leveling, this would still not
@@ -933,19 +1183,20 @@ As a flash device reaches the end of its life, the metadata blocks will
naturally be the first to go since they are updated most often. In this
situation, the littlefs is designed to simply move on to another set of
metadata blocks. This travelling means that at the end of a flash device's
life, the filesystem will have worn the device down as evenly as a dynamic
wear leveling filesystem could anyways. Simply put, if the lifetime of flash
is a serious concern, static wear leveling is the only valid solution.
life, the filesystem will have worn the device down nearly as evenly as the
usual dynamic wear leveling could. More aggressive wear leveling would come
with a code-size cost for marginal benefit.
This is a very important takeaway to note. If your storage stack uses highly
sensitive storage such as NAND flash. In most cases you are going to be better
off just using a [flash translation layer (FTL)](https://en.wikipedia.org/wiki/Flash_translation_layer).
One important takeaway to note, if your storage stack uses highly sensitive
storage such as NAND flash, static wear leveling is the only valid solution.
In most cases you are going to be better off using a full [flash translation layer (FTL)](https://en.wikipedia.org/wiki/Flash_translation_layer).
NAND flash already has many limitations that make it poorly suited for an
embedded system: low erase cycles, very large blocks, errors that can develop
even during reads, errors that can develop during writes of neighboring blocks.
Managing sensitive storage such as NAND flash is out of scope for the littlefs.
The littlefs does have some properties that may be beneficial on top of a FTL,
such as limiting the number of writes where possible. But if you have the
such as limiting the number of writes where possible, but if you have the
storage requirements that necessitate the need of NAND flash, you should have
the RAM to match and just use an FTL or flash filesystem.
@@ -955,18 +1206,18 @@ So, to summarize:
1. The littlefs is composed of directory blocks
2. Each directory is a linked-list of metadata pairs
3. These metadata pairs can be updated atomically by alternative which
3. These metadata pairs can be updated atomically by alternating which
metadata block is active
4. Directory blocks contain either references to other directories or files
5. Files are represented by copy-on-write CTZ linked-lists
6. The CTZ linked-lists support appending in O(1) and reading in O(n logn)
7. Blocks are allocated by scanning the filesystem for used blocks in a
5. Files are represented by copy-on-write CTZ skip-lists which support O(1)
append and O(nlogn) reading
6. Blocks are allocated by scanning the filesystem for used blocks in a
fixed-size lookahead region is that stored in a bit-vector
8. To facilitate scanning the filesystem, all directories are part of a
7. To facilitate scanning the filesystem, all directories are part of a
linked-list that is threaded through the entire filesystem
9. If a block develops an error, the littlefs allocates a new block, and
8. If a block develops an error, the littlefs allocates a new block, and
moves the data and references of the old block to the new.
10. Any case where an atomic operation is not possible, it is taken care of
9. Any case where an atomic operation is not possible, mistakes are resolved
by a deorphan step that occurs on the first allocation after boot
That's the little filesystem. Thanks for reading!

View File

@@ -11,6 +11,8 @@ ASM := $(SRC:.c=.s)
TEST := $(patsubst tests/%.sh,%,$(wildcard tests/test_*))
SHELL = /bin/bash -o pipefail
ifdef DEBUG
CFLAGS += -O0 -g3
else
@@ -34,7 +36,11 @@ size: $(OBJ)
test: test_format test_dirs test_files test_seek test_parallel \
test_alloc test_paths test_orphan test_move test_corrupt
test_%: tests/test_%.sh
ifdef QUIET
./$< | sed -n '/^[-=]/p'
else
./$<
endif
-include $(DEP)

View File

@@ -11,23 +11,17 @@ A little fail-safe filesystem designed for embedded systems.
| | |
```
**Fail-safe** - The littlefs is designed to work consistently with random
power failures. During filesystem operations the storage on disk is always
kept in a valid state. The filesystem also has strong copy-on-write garuntees.
When updating a file, the original file will remain unmodified until the
file is closed, or sync is called.
**Bounded RAM/ROM** - The littlefs is designed to work with a limited amount
of memory. Recursion is avoided and dynamic memory is limited to configurable
buffers that can be provided statically.
**Wear awareness** - While the littlefs does not implement static wear
leveling, the littlefs takes into account write errors reported by the
underlying block device and uses a limited form of dynamic wear leveling
to manage blocks that go bad during the lifetime of the filesystem.
**Power-loss resilient** - The littlefs is designed for systems that may have
random power failures. The littlefs has strong copy-on-write guaruntees and
storage on disk is always kept in a valid state.
**Bounded ram/rom** - The littlefs is designed to work in a
limited amount of memory, recursion is avoided, and dynamic memory is kept
to a minimum. The littlefs allocates two fixed-size buffers for general
operations, and one fixed-size buffer per file. If there is only ever one file
in use, all memory can be provided statically and the littlefs can be used
in a system without dynamic memory.
**Wear leveling** - Since the most common form of embedded storage is erodible
flash memories, littlefs provides a form of dynamic wear leveling for systems
that can not fit a full flash translation layer.
## Example
@@ -96,7 +90,7 @@ int main(void) {
Detailed documentation (or at least as much detail as is currently available)
can be cound in the comments in [lfs.h](lfs.h).
As you may have noticed, the littlefs takes in a configuration structure that
As you may have noticed, littlefs takes in a configuration structure that
defines how the filesystem operates. The configuration struct provides the
filesystem with the block device operations and dimensions, tweakable
parameters that tradeoff memory usage for performance, and optional
@@ -104,14 +98,16 @@ static buffers if the user wants to avoid dynamic memory.
The state of the littlefs is stored in the `lfs_t` type which is left up
to the user to allocate, allowing multiple filesystems to be in use
simultaneously. With the `lfs_t` and configuration struct, a user can either
simultaneously. With the `lfs_t` and configuration struct, a user can
format a block device or mount the filesystem.
Once mounted, the littlefs provides a full set of posix-like file and
directory functions, with the deviation that the allocation of filesystem
structures must be provided by the user. An important addition is that
no file updates will actually be written to disk until a sync or close
is called.
structures must be provided by the user.
All posix operations, such as remove and rename, are atomic, even in event
of power-loss. Additionally, no file updates are actually commited to the
filesystem until sync or close is called on the file.
## Other notes
@@ -119,20 +115,19 @@ All littlefs have the potential to return a negative error code. The errors
can be either one of those found in the `enum lfs_error` in [lfs.h](lfs.h),
or an error returned by the user's block device operations.
It should also be noted that the littlefs does not do anything to insure
that the data written to disk is machine portable. It should be fine as
long as the machines involved share endianness and don't have really
strange padding requirements. If the question does come up, the littlefs
metadata should be stored on disk in little-endian format.
It should also be noted that the current implementation of littlefs doesn't
really do anything to insure that the data written to disk is machine portable.
This is fine as long as all of the involved machines share endianness
(little-endian) and don't have strange padding requirements.
## Design
## Reference material
the littlefs was developed with the goal of learning more about filesystem
design by tackling the relative unsolved problem of managing a robust
filesystem resilient to power loss on devices with limited RAM and ROM.
More detail on the solutions and tradeoffs incorporated into this filesystem
can be found in [DESIGN.md](DESIGN.md). The specification for the layout
of the filesystem on disk can be found in [SPEC.md](SPEC.md).
[DESIGN.md](DESIGN.md) - DESIGN.md contains a fully detailed dive into how
littlefs actually works. I would encourage you to read it since the
solutions and tradeoffs at work here are quite interesting.
[SPEC.md](SPEC.md) - SPEC.md contains the on-disk specification of littlefs
with all the nitty-gritty details. Can be useful for developing tooling.
## Testing
@@ -143,3 +138,20 @@ The tests assume a linux environment and can be started with make:
``` bash
make test
```
## Related projects
[Mbed OS](https://github.com/ARMmbed/mbed-os/tree/master/features/filesystem/littlefs) -
The easiest way to get started with littlefs is to jump into [Mbed](https://os.mbed.com/),
which already has block device drivers for most forms of embedded storage. The
littlefs is available in Mbed OS as the [LittleFileSystem](https://os.mbed.com/docs/latest/reference/littlefilesystem.html)
class.
[littlefs-fuse](https://github.com/geky/littlefs-fuse) - A [FUSE](https://github.com/libfuse/libfuse)
wrapper for littlefs. The project allows you to mount littlefs directly in a
Linux machine. Can be useful for debugging littlefs if you have an SD card
handy.
[littlefs-js](https://github.com/geky/littlefs-js) - A javascript wrapper for
littlefs. I'm not sure why you would want this, but it is handy for demos.
You can see it in action [here](http://littlefs.geky.net/demo.html).

33
SPEC.md
View File

@@ -121,13 +121,18 @@ Here's the layout of entries on disk:
**Entry type** - Type of the entry, currently this is limited to the following:
- 0x11 - file entry
- 0x22 - directory entry
- 0xe2 - superblock entry
- 0x2e - superblock entry
Additionally, the type is broken into two 4 bit nibbles, with the lower nibble
Additionally, the type is broken into two 4 bit nibbles, with the upper nibble
specifying the type's data structure used when scanning the filesystem. The
upper nibble clarifies the type further when multiple entries share the same
lower nibble clarifies the type further when multiple entries share the same
data structure.
The highest bit is reserved for marking the entry as "moved". If an entry
is marked as "moved", the entry may also exist somewhere else in the
filesystem. If the entry exists elsewhere, this entry must be treated as
though it does not exist.
**Entry length** - Length in bytes of the entry-specific data. This does
not include the entry type size, attributes, or name. The full size in bytes
of the entry is 4 + entry length + attribute length + name length.
@@ -175,7 +180,7 @@ Here's the layout of the superblock entry:
| offset | size | description |
|--------|------------------------|----------------------------------------|
| 0x00 | 8 bits | entry type (0xe2 for superblock entry) |
| 0x00 | 8 bits | entry type (0x2e for superblock entry) |
| 0x01 | 8 bits | entry length (20 bytes) |
| 0x02 | 8 bits | attribute length |
| 0x03 | 8 bits | name length (8 bytes) |
@@ -208,7 +213,7 @@ Here's an example of a complete superblock:
(32 bits) revision count = 3 (0x00000003)
(32 bits) dir size = 52 bytes, end of dir (0x00000034)
(64 bits) tail pointer = 3, 2 (0x00000003, 0x00000002)
(8 bits) entry type = superblock (0xe2)
(8 bits) entry type = superblock (0x2e)
(8 bits) entry length = 20 bytes (0x14)
(8 bits) attribute length = 0 bytes (0x00)
(8 bits) name length = 8 bytes (0x08)
@@ -220,7 +225,7 @@ Here's an example of a complete superblock:
(32 bits) crc = 0xc50b74fa
00000000: 03 00 00 00 34 00 00 00 03 00 00 00 02 00 00 00 ....4...........
00000010: e2 14 00 08 03 00 00 00 02 00 00 00 00 02 00 00 ................
00000010: 2e 14 00 08 03 00 00 00 02 00 00 00 00 02 00 00 ................
00000020: 00 04 00 00 01 00 01 00 6c 69 74 74 6c 65 66 73 ........littlefs
00000030: fa 74 0b c5 .t..
```
@@ -262,15 +267,19 @@ Here's an example of a directory entry:
Files are stored in entries with a pointer to the head of the file and the
size of the file. This is enough information to determine the state of the
CTZ linked-list that is being referenced.
CTZ skip-list that is being referenced.
How files are actually stored on disk is a bit complicated. The full
explanation of CTZ linked-lists can be found in [DESIGN.md](DESIGN.md#ctz-linked-lists).
explanation of CTZ skip-lists can be found in [DESIGN.md](DESIGN.md#ctz-skip-lists).
A terribly quick summary: For every nth block where n is divisible by 2^x,
the block contains a pointer that points x blocks towards the beginning of the
file. These pointers are stored in order of x in each block of the file
immediately before the data in the block.
the block contains a pointer to block n-2^x. These pointers are stored in
increasing order of x in each block of the file preceding the data in the
block.
The maximum number of pointers in a block is bounded by the maximum file size
divided by the block size. With 32 bits for file size, this results in a
minimum block size of 104 bytes.
Here's the layout of a file entry:
@@ -286,7 +295,7 @@ Here's the layout of a file entry:
| 0xc+a | name length bytes | directory name |
**File head** - Pointer to the block that is the head of the file's CTZ
linked-list.
skip-list.
**File size** - Size of file in bytes.

View File

@@ -1,8 +1,19 @@
/*
* Block device emulated on standard files
*
* Copyright (c) 2017 Christopher Haster
* Distributed under the Apache 2.0 license
* Copyright (c) 2017 ARM Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "emubd/lfs_emubd.h"
@@ -127,8 +138,8 @@ int lfs_emubd_prog(const struct lfs_config *cfg, lfs_block_t block,
snprintf(emu->child, LFS_NAME_MAX, "%x", block);
FILE *f = fopen(emu->path, "r+b");
if (!f && errno != ENOENT) {
return -errno;
if (!f) {
return (errno == EACCES) ? 0 : -errno;
}
// Check that file was erased
@@ -178,14 +189,14 @@ int lfs_emubd_erase(const struct lfs_config *cfg, lfs_block_t block) {
return -errno;
}
if (!err && S_ISREG(st.st_mode)) {
if (!err && S_ISREG(st.st_mode) && (S_IWUSR & st.st_mode)) {
int err = unlink(emu->path);
if (err) {
return -errno;
}
}
if (err || S_ISREG(st.st_mode)) {
if (errno == ENOENT || (S_ISREG(st.st_mode) && (S_IWUSR & st.st_mode))) {
FILE *f = fopen(emu->path, "w");
if (!f) {
return -errno;

View File

@@ -1,8 +1,19 @@
/*
* Block device emulated on standard files
*
* Copyright (c) 2017 Christopher Haster
* Distributed under the Apache 2.0 license
* Copyright (c) 2017 ARM Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef LFS_EMUBD_H
#define LFS_EMUBD_H

549
lfs.c
View File

@@ -1,8 +1,19 @@
/*
* The little filesystem
*
* Copyright (c) 2017 Christopher Haster
* Distributed under the Apache 2.0 license
* Copyright (c) 2017 ARM Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "lfs.h"
#include "lfs_util.h"
@@ -267,7 +278,7 @@ static int lfs_alloc_lookahead(void *p, lfs_block_t block) {
% (lfs_soff_t)(lfs->cfg->block_count))
+ lfs->cfg->block_count) % lfs->cfg->block_count;
if (off < lfs->free.lookahead) {
if (off < lfs->cfg->lookahead) {
lfs->free.buffer[off / 32] |= 1U << (off % 32);
}
@@ -283,7 +294,8 @@ static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) {
return LFS_ERR_NOSPC;
}
if (lfs->free.off >= lfs->free.lookahead) {
if (lfs->free.off >= lfs_min(
lfs->cfg->lookahead, lfs->cfg->block_count)) {
break;
}
@@ -297,11 +309,11 @@ static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) {
}
}
lfs->free.begin += lfs->free.lookahead;
lfs->free.begin += lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count);
lfs->free.off = 0;
// find mask of free blocks from tree
memset(lfs->free.buffer, 0, lfs->free.lookahead/8);
memset(lfs->free.buffer, 0, lfs->cfg->lookahead/8);
int err = lfs_traverse(lfs, lfs_alloc_lookahead, lfs);
if (err) {
return err;
@@ -358,12 +370,12 @@ static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) {
if (err) {
return err;
}
lfs->sum += dir->d.rev;
// set defaults
dir->d.rev += 1;
dir->d.size = sizeof(dir->d)+4;
dir->d.tail[0] = -1;
dir->d.tail[1] = -1;
dir->d.tail[0] = 0xffffffff;
dir->d.tail[1] = 0xffffffff;
dir->off = sizeof(dir->d);
// don't write out yet, let caller take care of that
@@ -431,10 +443,8 @@ struct lfs_region {
static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir,
const struct lfs_region *regions, int count) {
// increment rev such that, even == stable, odd == unstable
uint32_t diff = 1 + ((lfs->sum & 0x1) ^ !lfs->unstable);
dir->d.rev += diff;
lfs->sum += diff;
// increment revision count
dir->d.rev += 1;
// keep pairs in order such that pair[0] is most recent
lfs_pairswap(dir->pair);
@@ -446,90 +456,94 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir,
bool relocated = false;
while (true) {
int err = lfs_bd_erase(lfs, dir->pair[0]);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
if (true) {
int err = lfs_bd_erase(lfs, dir->pair[0]);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
return err;
}
uint32_t crc = 0xffffffff;
lfs_crc(&crc, &dir->d, sizeof(dir->d));
err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d));
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
uint32_t crc = 0xffffffff;
lfs_crc(&crc, &dir->d, sizeof(dir->d));
err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d));
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
return err;
}
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);
int err = lfs_bd_prog(lfs, dir->pair[0],
newoff, regions[i].newdata, regions[i].newlen);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
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);
int 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;
}
return err;
}
oldoff += regions[i].oldlen;
newoff += regions[i].newlen;
i += 1;
} else {
uint8_t data;
int 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;
oldoff += regions[i].oldlen;
newoff += regions[i].newlen;
i += 1;
} else {
uint8_t data;
int err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1);
if (err) {
return 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;
}
oldoff += 1;
newoff += 1;
}
}
err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4);
if (err) {
if (err == LFS_ERR_CORRUPT) {
err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
err = lfs_bd_sync(lfs);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
// successful commit, check checksum to make sure
uint32_t ncrc = 0xffffffff;
err = lfs_bd_crc(lfs, dir->pair[0], 0,
(0x7fffffff & dir->d.size)-4, &ncrc);
if (err) {
return err;
}
if (ncrc != crc) {
goto relocate;
}
return err;
}
err = lfs_bd_sync(lfs);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
// successful commit, check checksum to make sure
crc = 0xffffffff;
err = lfs_bd_crc(lfs, dir->pair[0], 0, 0x7fffffff & dir->d.size, &crc);
if (err) {
return err;
}
if (crc == 0) {
break;
}
break;
relocate:
//commit was corrupted
LFS_DEBUG("Bad block at %d", dir->pair[0]);
@@ -545,7 +559,7 @@ relocate:
}
// relocate half of pair
err = lfs_alloc(lfs, &dir->pair[0]);
int err = lfs_alloc(lfs, &dir->pair[0]);
if (err) {
return err;
}
@@ -555,7 +569,18 @@ relocate:
// update references if we relocated
LFS_DEBUG("Relocating %d %d to %d %d",
oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]);
return lfs_relocate(lfs, oldpair, dir->pair);
int err = lfs_relocate(lfs, oldpair, dir->pair);
if (err) {
return err;
}
}
// shift over any directories that are affected
for (lfs_dir_t *d = lfs->dirs; d; d = d->next) {
if (lfs_paircmp(d->pair, dir->pair) == 0) {
d->pair[0] = dir->pair[0];
d->pair[1] = dir->pair[1];
}
}
return 0;
@@ -614,7 +639,7 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
}
static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
// either shift out the one entry or remove the whole dir block
// check if we should just drop the directory block
if ((dir->d.size & 0x7fffffff) == sizeof(dir->d)+4
+ lfs_entry_size(entry)) {
lfs_dir_t pdir;
@@ -623,39 +648,44 @@ static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
return res;
}
if (!(pdir.d.size & 0x80000000)) {
return lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, lfs_entry_size(entry), NULL, 0},
}, 1);
} else {
lfs->sum -= dir->d.rev;
if (pdir.d.size & 0x80000000) {
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, 0);
}
} else {
int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, lfs_entry_size(entry), NULL, 0},
}, 1);
if (err) {
return err;
}
}
// shift over any files 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->pair[0] = 0xffffffff;
f->pair[1] = 0xffffffff;
} else if (f->poff > entry->off) {
f->poff -= lfs_entry_size(entry);
}
// shift out the entry
int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, lfs_entry_size(entry), NULL, 0},
}, 1);
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->pair[0] = 0xffffffff;
f->pair[1] = 0xffffffff;
} else if (f->poff > entry->off) {
f->poff -= lfs_entry_size(entry);
}
}
return 0;
}
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 -= lfs_entry_size(entry);
d->pos -= lfs_entry_size(entry);
}
}
}
return 0;
}
static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
@@ -783,15 +813,13 @@ static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir,
return err;
}
}
return 0;
}
/// Top level directory operations ///
int lfs_mkdir(lfs_t *lfs, const char *path) {
// deorphan if we haven't yet, needed at most once after poweron
if (lfs->unstable) {
if (!lfs->deorphaned) {
int err = lfs_deorphan(lfs);
if (err) {
return err;
@@ -808,7 +836,7 @@ int lfs_mkdir(lfs_t *lfs, const char *path) {
lfs_entry_t entry;
err = lfs_dir_find(lfs, &cwd, &entry, &path);
if (err != LFS_ERR_NOENT || strchr(path, '/') != NULL) {
return err ? err : LFS_ERR_EXISTS;
return err ? err : LFS_ERR_EXIST;
}
// build up new directory
@@ -883,11 +911,23 @@ int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) {
dir->head[1] = dir->pair[1];
dir->pos = sizeof(dir->d) - 2;
dir->off = sizeof(dir->d);
// add to list of directories
dir->next = lfs->dirs;
lfs->dirs = dir;
return 0;
}
int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) {
// do nothing, dir is always synchronized
// remove from list of directories
for (lfs_dir_t **p = &lfs->dirs; *p; p = &(*p)->next) {
if (*p == dir) {
*p = dir->next;
break;
}
}
return 0;
}
@@ -995,37 +1035,36 @@ int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) {
/// File index list operations ///
static int lfs_index(lfs_t *lfs, lfs_off_t *off) {
lfs_off_t i = 0;
lfs_size_t words = lfs->cfg->block_size / 4;
while (*off >= lfs->cfg->block_size) {
i += 1;
*off -= lfs->cfg->block_size;
*off += 4*lfs_min(lfs_ctz(i)+1, words-1);
static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) {
lfs_off_t size = *off;
lfs_off_t b = lfs->cfg->block_size - 2*4;
lfs_off_t i = size / b;
if (i == 0) {
return 0;
}
i = (size - 4*(lfs_popc(i-1)+2)) / b;
*off = size - b*i - 4*lfs_popc(i);
return i;
}
static int lfs_index_find(lfs_t *lfs,
static int lfs_ctz_find(lfs_t *lfs,
lfs_cache_t *rcache, const lfs_cache_t *pcache,
lfs_block_t head, lfs_size_t size,
lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) {
if (size == 0) {
*block = -1;
*block = 0xffffffff;
*off = 0;
return 0;
}
lfs_off_t current = lfs_index(lfs, &(lfs_off_t){size-1});
lfs_off_t target = lfs_index(lfs, &pos);
lfs_size_t words = lfs->cfg->block_size / 4;
lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size-1});
lfs_off_t target = lfs_ctz_index(lfs, &pos);
while (current > target) {
lfs_size_t skip = lfs_min(
lfs_npw2(current-target+1) - 1,
lfs_min(lfs_ctz(current)+1, words-1) - 1);
lfs_ctz(current));
int err = lfs_cache_read(lfs, rcache, pcache, head, 4*skip, &head, 4);
if (err) {
@@ -1041,65 +1080,21 @@ static int lfs_index_find(lfs_t *lfs,
return 0;
}
static int lfs_index_extend(lfs_t *lfs,
static int lfs_ctz_extend(lfs_t *lfs,
lfs_cache_t *rcache, lfs_cache_t *pcache,
lfs_block_t head, lfs_size_t size,
lfs_off_t *block, lfs_block_t *off) {
lfs_block_t *block, lfs_off_t *off) {
while (true) {
// go ahead and grab a block
int err = lfs_alloc(lfs, block);
lfs_block_t nblock;
int err = lfs_alloc(lfs, &nblock);
if (err) {
return err;
}
assert(*block >= 2 && *block <= lfs->cfg->block_count);
assert(nblock >= 2 && nblock <= lfs->cfg->block_count);
err = lfs_bd_erase(lfs, *block);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
if (size == 0) {
*off = 0;
return 0;
}
size -= 1;
lfs_off_t index = lfs_index(lfs, &size);
size += 1;
// just copy out the last block if it is incomplete
if (size != lfs->cfg->block_size) {
for (lfs_off_t i = 0; i < size; i++) {
uint8_t data;
int err = lfs_cache_read(lfs, rcache, NULL, head, i, &data, 1);
if (err) {
return err;
}
err = lfs_cache_prog(lfs, pcache, rcache, *block, i, &data, 1);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
}
*off = size;
return 0;
}
// append block
index += 1;
lfs_size_t words = lfs->cfg->block_size / 4;
lfs_size_t skips = lfs_min(lfs_ctz(index)+1, words-1);
for (lfs_off_t i = 0; i < skips; i++) {
int err = lfs_cache_prog(lfs, pcache, rcache,
*block, 4*i, &head, 4);
if (true) {
err = lfs_bd_erase(lfs, nblock);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
@@ -1107,28 +1102,80 @@ static int lfs_index_extend(lfs_t *lfs,
return err;
}
if (i != skips-1) {
err = lfs_cache_read(lfs, rcache, NULL, head, 4*i, &head, 4);
if (err) {
return err;
}
if (size == 0) {
*block = nblock;
*off = 0;
return 0;
}
assert(head >= 2 && head <= lfs->cfg->block_count);
size -= 1;
lfs_off_t index = lfs_ctz_index(lfs, &size);
size += 1;
// just copy out the last block if it is incomplete
if (size != lfs->cfg->block_size) {
for (lfs_off_t i = 0; i < size; i++) {
uint8_t data;
int err = lfs_cache_read(lfs, rcache, NULL,
head, i, &data, 1);
if (err) {
return err;
}
err = lfs_cache_prog(lfs, pcache, rcache,
nblock, i, &data, 1);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
}
*block = nblock;
*off = size;
return 0;
}
// append block
index += 1;
lfs_size_t skips = lfs_ctz(index) + 1;
for (lfs_off_t i = 0; i < skips; i++) {
int err = lfs_cache_prog(lfs, pcache, rcache,
nblock, 4*i, &head, 4);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
if (i != skips-1) {
err = lfs_cache_read(lfs, rcache, NULL,
head, 4*i, &head, 4);
if (err) {
return err;
}
}
assert(head >= 2 && head <= lfs->cfg->block_count);
}
*block = nblock;
*off = 4*skips;
return 0;
}
*off = 4*skips;
return 0;
relocate:
LFS_DEBUG("Bad block at %d", *block);
LFS_DEBUG("Bad block at %d", nblock);
// just clear cache and try a new block
pcache->block = 0xffffffff;
}
}
static int lfs_index_traverse(lfs_t *lfs,
static int lfs_ctz_traverse(lfs_t *lfs,
lfs_cache_t *rcache, const lfs_cache_t *pcache,
lfs_block_t head, lfs_size_t size,
int (*cb)(void*, lfs_block_t), void *data) {
@@ -1136,7 +1183,7 @@ static int lfs_index_traverse(lfs_t *lfs,
return 0;
}
lfs_off_t index = lfs_index(lfs, &(lfs_off_t){size-1});
lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size-1});
while (true) {
int err = cb(data, head);
@@ -1148,15 +1195,23 @@ static int lfs_index_traverse(lfs_t *lfs,
return 0;
}
err = lfs_cache_read(lfs, rcache, pcache, head, 0, &head, 4);
lfs_block_t heads[2];
int count = 2 - (index & 1);
err = lfs_cache_read(lfs, rcache, pcache, head, 0, &heads, count*4);
if (err) {
return err;
}
index -= 1;
}
for (int i = 0; i < count-1; i++) {
err = cb(data, heads[i]);
if (err) {
return err;
}
}
return 0;
head = heads[count-1];
index -= count;
}
}
@@ -1164,7 +1219,7 @@ static int lfs_index_traverse(lfs_t *lfs,
int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
const char *path, int flags) {
// deorphan if we haven't yet, needed at most once after poweron
if ((flags & 3) != LFS_O_RDONLY && lfs->unstable) {
if ((flags & 3) != LFS_O_RDONLY && !lfs->deorphaned) {
int err = lfs_deorphan(lfs);
if (err) {
return err;
@@ -1194,7 +1249,7 @@ int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
entry.d.elen = sizeof(entry.d) - 4;
entry.d.alen = 0;
entry.d.nlen = strlen(path);
entry.d.u.file.head = -1;
entry.d.u.file.head = 0xffffffff;
entry.d.u.file.size = 0;
err = lfs_dir_append(lfs, &cwd, &entry, path);
if (err) {
@@ -1203,7 +1258,7 @@ int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
} else if (entry.d.type == LFS_TYPE_DIR) {
return LFS_ERR_ISDIR;
} else if (flags & LFS_O_EXCL) {
return LFS_ERR_EXISTS;
return LFS_ERR_EXIST;
}
// setup file struct
@@ -1216,7 +1271,10 @@ int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
file->pos = 0;
if (flags & LFS_O_TRUNC) {
file->head = -1;
if (file->size != 0) {
file->flags |= LFS_F_DIRTY;
}
file->head = 0xffffffff;
file->size = 0;
}
@@ -1387,7 +1445,9 @@ int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) {
return err;
}
if ((file->flags & LFS_F_DIRTY) && !lfs_pairisnull(file->pair)) {
if ((file->flags & LFS_F_DIRTY) &&
!(file->flags & LFS_F_ERRED) &&
!lfs_pairisnull(file->pair)) {
// update dir entry
lfs_dir_t cwd;
int err = lfs_dir_fetch(lfs, &cwd, file->pair);
@@ -1450,7 +1510,7 @@ lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file,
// check if we need a new block
if (!(file->flags & LFS_F_READING) ||
file->off == lfs->cfg->block_size) {
int err = lfs_index_find(lfs, &file->cache, NULL,
int err = lfs_ctz_find(lfs, &file->cache, NULL,
file->head, file->size,
file->pos, &file->block, &file->off);
if (err) {
@@ -1517,10 +1577,11 @@ lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
file->off == lfs->cfg->block_size) {
if (!(file->flags & LFS_F_WRITING) && file->pos > 0) {
// find out which block we're extending from
int err = lfs_index_find(lfs, &file->cache, NULL,
int err = lfs_ctz_find(lfs, &file->cache, NULL,
file->head, file->size,
file->pos-1, &file->block, &file->off);
if (err) {
file->flags |= LFS_F_ERRED;
return err;
}
@@ -1530,10 +1591,11 @@ lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
// extend file with new blocks
lfs_alloc_ack(lfs);
int err = lfs_index_extend(lfs, &lfs->rcache, &file->cache,
int err = lfs_ctz_extend(lfs, &lfs->rcache, &file->cache,
file->block, file->pos,
&file->block, &file->off);
if (err) {
file->flags |= LFS_F_ERRED;
return err;
}
@@ -1549,6 +1611,7 @@ lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
file->flags |= LFS_F_ERRED;
return err;
}
@@ -1556,6 +1619,7 @@ lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
relocate:
err = lfs_file_relocate(lfs, file);
if (err) {
file->flags |= LFS_F_ERRED;
return err;
}
}
@@ -1568,6 +1632,7 @@ relocate:
lfs_alloc_ack(lfs);
}
file->flags &= ~LFS_F_ERRED;
return size;
}
@@ -1583,13 +1648,13 @@ lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file,
if (whence == LFS_SEEK_SET) {
file->pos = off;
} else if (whence == LFS_SEEK_CUR) {
if (-off > file->pos) {
if (off < 0 && (lfs_off_t)-off > file->pos) {
return LFS_ERR_INVAL;
}
file->pos = file->pos + off;
} else if (whence == LFS_SEEK_END) {
if (-off > file->size) {
if (off < 0 && (lfs_off_t)-off > file->size) {
return LFS_ERR_INVAL;
}
@@ -1657,7 +1722,7 @@ int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) {
int lfs_remove(lfs_t *lfs, const char *path) {
// deorphan if we haven't yet, needed at most once after poweron
if (lfs->unstable) {
if (!lfs->deorphaned) {
int err = lfs_deorphan(lfs);
if (err) {
return err;
@@ -1685,12 +1750,11 @@ int lfs_remove(lfs_t *lfs, const char *path) {
if (err) {
return err;
} else if (dir.d.size != sizeof(dir.d)+4) {
return LFS_ERR_INVAL;
return LFS_ERR_NOTEMPTY;
}
}
// remove the entry
lfs->unstable += (entry.d.type == LFS_TYPE_DIR);
err = lfs_dir_remove(lfs, &cwd, &entry);
if (err) {
return err;
@@ -1704,11 +1768,9 @@ int lfs_remove(lfs_t *lfs, const char *path) {
}
assert(res); // must have pred
lfs->sum -= dir.d.rev;
cwd.d.tail[0] = dir.d.tail[0];
cwd.d.tail[1] = dir.d.tail[1];
lfs->unstable -= 1;
int err = lfs_dir_commit(lfs, &cwd, NULL, 0);
if (err) {
return err;
@@ -1720,7 +1782,7 @@ int lfs_remove(lfs_t *lfs, const char *path) {
int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
// deorphan if we haven't yet, needed at most once after poweron
if (lfs->unstable) {
if (!lfs->deorphaned) {
int err = lfs_deorphan(lfs);
if (err) {
return err;
@@ -1775,7 +1837,6 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
}
// mark as moving
lfs->unstable += 1 + (prevexists && preventry.d.type == LFS_TYPE_DIR);
oldentry.d.type |= 0x80;
err = lfs_dir_update(lfs, &oldcwd, &oldentry, NULL);
if (err) {
@@ -1811,7 +1872,6 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
}
// remove old entry
lfs->unstable -= 1;
err = lfs_dir_remove(lfs, &oldcwd, &oldentry);
if (err) {
return err;
@@ -1825,11 +1885,9 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
}
assert(res); // must have pred
lfs->sum -= dir.d.rev;
newcwd.d.tail[0] = dir.d.tail[0];
newcwd.d.tail[1] = dir.d.tail[1];
lfs->unstable -= 1;
int err = lfs_dir_commit(lfs, &newcwd, NULL, 0);
if (err) {
return err;
@@ -1867,24 +1925,31 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
}
// setup lookahead, round down to nearest 32-bits
lfs->free.lookahead = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count);
lfs->free.lookahead = 32 * (lfs->free.lookahead / 32);
assert(lfs->free.lookahead > 0);
assert(lfs->cfg->lookahead % 32 == 0);
assert(lfs->cfg->lookahead > 0);
if (lfs->cfg->lookahead_buffer) {
lfs->free.buffer = lfs->cfg->lookahead_buffer;
} else {
lfs->free.buffer = malloc(lfs->free.lookahead/8);
lfs->free.buffer = malloc(lfs->cfg->lookahead/8);
if (!lfs->free.buffer) {
return LFS_ERR_NOMEM;
}
}
// check that program and read sizes are multiples of the block size
assert(lfs->cfg->prog_size % lfs->cfg->read_size == 0);
assert(lfs->cfg->block_size % lfs->cfg->prog_size == 0);
// check that the block size is large enough to fit ctz pointers
assert(4*lfs_npw2(0xffffffff / (lfs->cfg->block_size-2*4))
<= lfs->cfg->block_size);
// setup default state
lfs->root[0] = 0xffffffff;
lfs->root[1] = 0xffffffff;
lfs->files = NULL;
lfs->unstable = false;
lfs->sum = 0;
lfs->dirs = NULL;
lfs->deorphaned = false;
return 0;
}
@@ -1913,10 +1978,10 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
}
// create free lookahead
memset(lfs->free.buffer, 0, lfs->free.lookahead/8);
memset(lfs->free.buffer, 0, lfs->cfg->lookahead/8);
lfs->free.begin = 0;
lfs->free.off = 0;
lfs->free.end = lfs->free.begin + lfs->cfg->block_count;
lfs->free.end = lfs->free.begin + lfs->free.off + lfs->cfg->block_count;
// create superblock dir
lfs_alloc_ack(lfs);
@@ -1992,9 +2057,9 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) {
}
// setup free lookahead
lfs->free.begin = -lfs->free.lookahead;
lfs->free.off = lfs->free.lookahead;
lfs->free.end = lfs->free.begin + lfs->cfg->block_count;
lfs->free.begin = -lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count);
lfs->free.off = -lfs->free.begin;
lfs->free.end = lfs->free.begin + lfs->free.off + lfs->cfg->block_count;
// load superblock
lfs_dir_t dir;
@@ -2018,33 +2083,15 @@ 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]);
return LFS_ERR_CORRUPT;
} else if (err) {
return err;
}
if (superblock.d.version > (0x00010001 | 0x0000ffff)) {
LFS_ERROR("Invalid version %d.%d\n",
LFS_ERROR("Invalid version %d.%d",
0xffff & (superblock.d.version >> 16),
0xffff & (superblock.d.version >> 0));
return LFS_ERR_INVAL;
}
// sum rev counts in fs, even = stable, odd = unstable
lfs->sum += dir.d.rev;
while (!lfs_pairisnull(dir.d.tail)) {
int err = lfs_dir_fetch(lfs, &dir, dir.d.tail);
if (err) {
return err;
}
lfs->sum += dir.d.rev;
}
if (lfs->sum & 0x1) {
LFS_DEBUG("Power-loss detected %x", lfs->sum);
lfs->unstable += 1;
}
return 0;
}
@@ -2087,7 +2134,7 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
dir.off += lfs_entry_size(&entry);
if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) {
int err = lfs_index_traverse(lfs, &lfs->rcache, NULL,
int err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL,
entry.d.u.file.head, entry.d.u.file.size, cb, data);
if (err) {
return err;
@@ -2106,7 +2153,7 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
// iterate over any open files
for (lfs_file_t *f = lfs->files; f; f = f->next) {
if (f->flags & LFS_F_DIRTY) {
int err = lfs_index_traverse(lfs, &lfs->rcache, &f->cache,
int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache,
f->head, f->size, cb, data);
if (err) {
return err;
@@ -2114,7 +2161,7 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
}
if (f->flags & LFS_F_WRITING) {
int err = lfs_index_traverse(lfs, &lfs->rcache, &f->cache,
int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache,
f->block, f->pos, cb, data);
if (err) {
return err;
@@ -2241,7 +2288,6 @@ static int lfs_relocate(lfs_t *lfs,
entry.d.u.dir[0] = newpair[0];
entry.d.u.dir[1] = newpair[1];
lfs->unstable += 1;
int err = lfs_dir_update(lfs, &parent, &entry, NULL);
if (err) {
return err;
@@ -2277,6 +2323,8 @@ static int lfs_relocate(lfs_t *lfs,
}
int lfs_deorphan(lfs_t *lfs) {
lfs->deorphaned = true;
if (lfs_pairisnull(lfs->root)) {
return 0;
}
@@ -2375,7 +2423,6 @@ int lfs_deorphan(lfs_t *lfs) {
memcpy(&pdir, &cwd, sizeof(pdir));
}
lfs->unstable -= 1;
return 0;
}

54
lfs.h
View File

@@ -1,8 +1,19 @@
/*
* The little filesystem
*
* Copyright (c) 2017 Christopher Haster
* Distributed under the Apache 2.0 license
* Copyright (c) 2017 ARM Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef LFS_H
#define LFS_H
@@ -30,16 +41,17 @@ typedef uint32_t lfs_block_t;
// Possible error codes, these are negative to allow
// valid positive return values
enum lfs_error {
LFS_ERR_OK = 0, // No error
LFS_ERR_IO = -5, // Error during device operation
LFS_ERR_CORRUPT = -52, // Corrupted
LFS_ERR_NOENT = -2, // No directory entry
LFS_ERR_EXISTS = -17, // Entry already exists
LFS_ERR_NOTDIR = -20, // Entry is not a dir
LFS_ERR_ISDIR = -21, // Entry is a dir
LFS_ERR_INVAL = -22, // Invalid parameter
LFS_ERR_NOSPC = -28, // No space left on device
LFS_ERR_NOMEM = -12, // No more memory available
LFS_ERR_OK = 0, // No error
LFS_ERR_IO = -5, // Error during device operation
LFS_ERR_CORRUPT = -52, // Corrupted
LFS_ERR_NOENT = -2, // No directory entry
LFS_ERR_EXIST = -17, // Entry already exists
LFS_ERR_NOTDIR = -20, // Entry is not a dir
LFS_ERR_ISDIR = -21, // Entry is a dir
LFS_ERR_NOTEMPTY = -39, // Dir is not empty
LFS_ERR_INVAL = -22, // Invalid parameter
LFS_ERR_NOSPC = -28, // No space left on device
LFS_ERR_NOMEM = -12, // No more memory available
};
// File types
@@ -64,6 +76,7 @@ enum lfs_open_flags {
LFS_F_DIRTY = 0x10000, // File does not match storage
LFS_F_WRITING = 0x20000, // File has been written since last flush
LFS_F_READING = 0x40000, // File has been read since last flush
LFS_F_ERRED = 0x80000, // An error occured during write
};
// File seek flags
@@ -87,14 +100,14 @@ struct lfs_config {
// Program a region in a block. The block must have previously
// been erased. Negative error codes are propogated to the user.
// The prog function must return LFS_ERR_CORRUPT if the block should
// be considered bad.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
int (*prog)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size);
// Erase a block. A block must be erased before being programmed.
// The state of an erased block is undefined. Negative error codes
// are propogated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
int (*erase)(const struct lfs_config *c, lfs_block_t block);
// Sync the state of the underlying block device. Negative error codes
@@ -109,11 +122,13 @@ struct lfs_config {
// Minimum size of a block program. This determines the size of program
// buffers. This may be larger than the physical program size to improve
// performance by caching more of the block device.
// Must be a multiple of the read size.
lfs_size_t prog_size;
// Size of an erasable block. This does not impact ram consumption and
// may be larger than the physical erase size. However, this should be
// kept small as each file currently takes up an entire block .
// kept small as each file currently takes up an entire block.
// Must be a multiple of the program size.
lfs_size_t block_size;
// Number of erasable blocks on the device.
@@ -195,6 +210,7 @@ typedef struct lfs_file {
} lfs_file_t;
typedef struct lfs_dir {
struct lfs_dir *next;
lfs_block_t pair[2];
lfs_off_t off;
@@ -225,7 +241,6 @@ typedef struct lfs_superblock {
} lfs_superblock_t;
typedef struct lfs_free {
lfs_size_t lookahead;
lfs_block_t begin;
lfs_block_t end;
lfs_block_t off;
@@ -238,13 +253,13 @@ typedef struct lfs {
lfs_block_t root[2];
lfs_file_t *files;
lfs_dir_t *dirs;
lfs_cache_t rcache;
lfs_cache_t pcache;
lfs_free_t free;
uint8_t unstable;
uint8_t sum;
bool deorphaned;
} lfs_t;
@@ -435,8 +450,5 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data);
// Returns a negative error code on failure.
int lfs_deorphan(lfs_t *lfs);
// TODO doc
int lfs_deduplicate(lfs_t *lfs);
#endif

View File

@@ -1,8 +1,19 @@
/*
* lfs util functions
*
* Copyright (c) 2017 Christopher Haster
* Distributed under the Apache 2.0 license
* Copyright (c) 2017 ARM Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "lfs_util.h"

View File

@@ -1,8 +1,19 @@
/*
* lfs utility functions
*
* Copyright (c) 2017 Christopher Haster
* Distributed under the Apache 2.0 license
* Copyright (c) 2017 ARM Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef LFS_UTIL_H
#define LFS_UTIL_H
@@ -30,6 +41,10 @@ static inline uint32_t lfs_npw2(uint32_t a) {
return 32 - __builtin_clz(a-1);
}
static inline uint32_t lfs_popc(uint32_t a) {
return __builtin_popcount(a);
}
static inline int lfs_scmp(uint32_t a, uint32_t b) {
return (int)(unsigned)(a - b);
}

View File

@@ -121,6 +121,7 @@ tests/test.py << TEST
size = strlen("exhaustion");
memcpy(buffer, "exhaustion", size);
lfs_file_write(&lfs, &file[0], buffer, size) => size;
lfs_file_sync(&lfs, &file[0]) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
@@ -142,6 +143,7 @@ tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_RDONLY);
size = strlen("exhaustion");
lfs_file_size(&lfs, &file[0]) => size;
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "exhaustion", size) => 0;
lfs_file_close(&lfs, &file[0]) => 0;
@@ -166,6 +168,7 @@ tests/test.py << TEST
size = strlen("exhaustion");
memcpy(buffer, "exhaustion", size);
lfs_file_write(&lfs, &file[0], buffer, size) => size;
lfs_file_sync(&lfs, &file[0]) => 0;
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
@@ -187,6 +190,7 @@ tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_RDONLY);
size = strlen("exhaustion");
lfs_file_size(&lfs, &file[0]) => size;
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "exhaustion", size) => 0;
lfs_file_close(&lfs, &file[0]) => 0;
@@ -196,14 +200,14 @@ TEST
echo "--- Dir exhaustion test ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_stat(&lfs, "exhaustion", &info) => 0;
lfs_size_t fullsize = info.size;
lfs_remove(&lfs, "exhaustion") => 0;
lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0; i < fullsize - 2*512; i += size) {
for (lfs_size_t i = 0;
i < (cfg.block_count-6)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
@@ -214,7 +218,11 @@ tests/test.py << TEST
lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_APPEND);
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
lfs_file_write(&lfs, &file[0], buffer, size) => size;
for (lfs_size_t i = 0;
i < (cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
lfs_mkdir(&lfs, "exhaustiondir") => LFS_ERR_NOSPC;
@@ -224,14 +232,14 @@ TEST
echo "--- Chained dir exhaustion test ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_stat(&lfs, "exhaustion", &info) => 0;
lfs_size_t fullsize = info.size;
lfs_remove(&lfs, "exhaustion") => 0;
lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0; i < fullsize - 19*512; i += size) {
for (lfs_size_t i = 0;
i < (cfg.block_count-24)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
@@ -247,7 +255,9 @@ tests/test.py << TEST
lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
size = strlen("blahblahblahblah");
memcpy(buffer, "blahblahblahblah", size);
for (lfs_size_t i = 0; i < fullsize - 20*512; i += size) {
for (lfs_size_t i = 0;
i < (cfg.block_count-26)*(cfg.block_size-8);
i += size) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;

View File

@@ -82,6 +82,17 @@ do
lfs_chktree
done
echo "--- Block persistance ---"
for i in {0..33}
do
rm -rf blocks
mkdir blocks
lfs_mktree
chmod a-w blocks/$(printf '%x' $i)
lfs_mktree
lfs_chktree
done
echo "--- Big region corruption ---"
rm -rf blocks
mkdir blocks

View File

@@ -56,7 +56,7 @@ TEST
echo "--- Directory failures ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_mkdir(&lfs, "potato") => LFS_ERR_EXISTS;
lfs_mkdir(&lfs, "potato") => LFS_ERR_EXIST;
lfs_dir_open(&lfs, &dir[0], "tomato") => LFS_ERR_NOENT;
lfs_dir_open(&lfs, &dir[0], "burito") => LFS_ERR_NOTDIR;
lfs_file_open(&lfs, &file[0], "tomato", LFS_O_RDONLY) => LFS_ERR_NOENT;
@@ -126,7 +126,7 @@ TEST
echo "--- Directory remove ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_remove(&lfs, "potato") => LFS_ERR_INVAL;
lfs_remove(&lfs, "potato") => LFS_ERR_NOTEMPTY;
lfs_remove(&lfs, "potato/sweet") => 0;
lfs_remove(&lfs, "potato/baked") => 0;
lfs_remove(&lfs, "potato/fried") => 0;
@@ -255,7 +255,7 @@ tests/test.py << TEST
lfs_rename(&lfs, "warmpotato/baked", "coldpotato/baked") => 0;
lfs_rename(&lfs, "warmpotato/sweet", "coldpotato/sweet") => 0;
lfs_rename(&lfs, "warmpotato/fried", "coldpotato/fried") => 0;
lfs_remove(&lfs, "coldpotato") => LFS_ERR_INVAL;
lfs_remove(&lfs, "coldpotato") => LFS_ERR_NOTEMPTY;
lfs_remove(&lfs, "warmpotato") => 0;
lfs_unmount(&lfs) => 0;
TEST
@@ -282,10 +282,53 @@ tests/test.py << TEST
lfs_unmount(&lfs) => 0;
TEST
echo "--- Recursive remove ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_remove(&lfs, "coldpotato") => LFS_ERR_NOTEMPTY;
lfs_dir_open(&lfs, &dir[0], "coldpotato") => 0;
lfs_dir_read(&lfs, &dir[0], &info) => 1;
lfs_dir_read(&lfs, &dir[0], &info) => 1;
while (true) {
int err = lfs_dir_read(&lfs, &dir[0], &info);
err >= 0 => 1;
if (err == 0) {
break;
}
strcpy((char*)buffer, "coldpotato/");
strcat((char*)buffer, info.name);
lfs_remove(&lfs, (char*)buffer) => 0;
}
lfs_remove(&lfs, "coldpotato") => 0;
TEST
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_dir_open(&lfs, &dir[0], "/") => 0;
lfs_dir_read(&lfs, &dir[0], &info) => 1;
strcmp(info.name, ".") => 0;
info.type => LFS_TYPE_DIR;
lfs_dir_read(&lfs, &dir[0], &info) => 1;
strcmp(info.name, "..") => 0;
info.type => LFS_TYPE_DIR;
lfs_dir_read(&lfs, &dir[0], &info) => 1;
strcmp(info.name, "burito") => 0;
info.type => LFS_TYPE_REG;
lfs_dir_read(&lfs, &dir[0], &info) => 1;
strcmp(info.name, "cactus") => 0;
info.type => LFS_TYPE_DIR;
lfs_dir_read(&lfs, &dir[0], &info) => 0;
lfs_dir_close(&lfs, &dir[0]) => 0;
lfs_unmount(&lfs) => 0;
TEST
echo "--- Multi-block remove ---"
tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0;
lfs_remove(&lfs, "cactus") => LFS_ERR_INVAL;
lfs_remove(&lfs, "cactus") => LFS_ERR_NOTEMPTY;
for (int i = 0; i < $LARGESIZE; i++) {
sprintf((char*)buffer, "cactus/test%d", i);
@@ -307,9 +350,6 @@ tests/test.py << TEST
lfs_dir_read(&lfs, &dir[0], &info) => 1;
strcmp(info.name, "burito") => 0;
info.type => LFS_TYPE_REG;
lfs_dir_read(&lfs, &dir[0], &info) => 1;
strcmp(info.name, "coldpotato") => 0;
info.type => LFS_TYPE_DIR;
lfs_dir_read(&lfs, &dir[0], &info) => 0;
lfs_dir_close(&lfs, &dir[0]) => 0;
lfs_unmount(&lfs) => 0;

View File

@@ -34,7 +34,8 @@ tests/test.py << TEST
lfs_size_t chunk = 31;
srand(0);
lfs_mount(&lfs, &cfg) => 0;
lfs_file_open(&lfs, &file[0], "$2", LFS_O_WRONLY | LFS_O_CREAT) => 0;
lfs_file_open(&lfs, &file[0], "$2",
${3:-LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC}) => 0;
for (lfs_size_t i = 0; i < size; i += chunk) {
chunk = (chunk < size - i) ? chunk : size - i;
for (lfs_size_t b = 0; b < chunk; b++) {
@@ -53,7 +54,10 @@ tests/test.py << TEST
lfs_size_t chunk = 29;
srand(0);
lfs_mount(&lfs, &cfg) => 0;
lfs_file_open(&lfs, &file[0], "$2", LFS_O_RDONLY) => 0;
lfs_stat(&lfs, "$2", &info) => 0;
info.type => LFS_TYPE_REG;
info.size => size;
lfs_file_open(&lfs, &file[0], "$2", ${3:-LFS_O_RDONLY}) => 0;
for (lfs_size_t i = 0; i < size; i += chunk) {
chunk = (chunk < size - i) ? chunk : size - i;
lfs_file_read(&lfs, &file[0], buffer, chunk) => chunk;
@@ -78,10 +82,27 @@ echo "--- Large file test ---"
w_test $LARGESIZE largeavacado
r_test $LARGESIZE largeavacado
echo "--- Zero file test ---"
w_test 0 noavacado
r_test 0 noavacado
echo "--- Truncate small test ---"
w_test $SMALLSIZE mediumavacado
r_test $SMALLSIZE mediumavacado
w_test $MEDIUMSIZE mediumavacado
r_test $MEDIUMSIZE mediumavacado
echo "--- Truncate zero test ---"
w_test $SMALLSIZE noavacado
r_test $SMALLSIZE noavacado
w_test 0 noavacado
r_test 0 noavacado
echo "--- Non-overlap check ---"
r_test $SMALLSIZE smallavacado
r_test $MEDIUMSIZE mediumavacado
r_test $LARGESIZE largeavacado
r_test 0 noavacado
echo "--- Dir check ---"
tests/test.py << TEST
@@ -105,6 +126,10 @@ tests/test.py << TEST
strcmp(info.name, "largeavacado") => 0;
info.type => LFS_TYPE_REG;
info.size => $LARGESIZE;
lfs_dir_read(&lfs, &dir[0], &info) => 1;
strcmp(info.name, "noavacado") => 0;
info.type => LFS_TYPE_REG;
info.size => 0;
lfs_dir_read(&lfs, &dir[0], &info) => 0;
lfs_dir_close(&lfs, &dir[0]) => 0;
lfs_unmount(&lfs) => 0;

View File

@@ -133,6 +133,14 @@ tests/test.py << TEST
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "kittycatcat", size) => 0;
lfs_file_seek(&lfs, &file[0], 0, LFS_SEEK_CUR) => size;
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "kittycatcat", size) => 0;
lfs_file_seek(&lfs, &file[0], size, LFS_SEEK_CUR) => 3*size;
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "kittycatcat", size) => 0;
lfs_file_seek(&lfs, &file[0], pos, LFS_SEEK_SET) => pos;
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "kittycatcat", size) => 0;
@@ -174,6 +182,14 @@ tests/test.py << TEST
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "kittycatcat", size) => 0;
lfs_file_seek(&lfs, &file[0], 0, LFS_SEEK_CUR) => size;
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "kittycatcat", size) => 0;
lfs_file_seek(&lfs, &file[0], size, LFS_SEEK_CUR) => 3*size;
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "kittycatcat", size) => 0;
lfs_file_seek(&lfs, &file[0], pos, LFS_SEEK_SET) => pos;
lfs_file_read(&lfs, &file[0], buffer, size) => size;
memcmp(buffer, "kittycatcat", size) => 0;