From 451c3d137fbd49ce5e00e765af66c16bb5169ee9 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Thu, 22 Feb 2018 18:40:26 -0600 Subject: [PATCH] Squashed 'features/filesystem/littlefs/littlefs/' changes from 5ee20e8..b2124a5 b2124a5 Fixed multiple deploy steps in Travis 949015a Merge pull request #28 from geky/configurables 67daf9e Added cross-compile targets for testing a3fd2d4 Added more configurable utils a0a55fb Added conversion to/from little-endian on disk 4f08424 Added software implementations of bitwise instructions 59ce49f Merge pull request #26 from Sim4n6/master 2f8ae34 Added a git ignore file with .o .d blocks dir and lfs bin e611cf5 Fix incorrect lookahead population before ack a25743a Fixed some minor error code differences 6716b55 Fixed error check when truncating files to larger size 809ffde Merge pull request #24 from aldot/silence-shadow-warnings-1 dc513b1 Silenced more of aldot's warnings aa50e03 Commentary typo fix 6d55755 tests: Silence warnings in template 029361e Silence shadow warnings fd04ed4 Added autogenerated release notes from commits 3101bc9 Do not print command invocation if QUIET d82e34c Merge pull request #21 from aldot/doc-tweaks 436707c doc: Editorial tweaks 3457252 doc: Spelling fixes 6d8e0e2 Moved -Werror flag to CI only 88f678f Fixed self-assign warning in tests 3ef4847 Added remove step in tests to force rebuild f694b14 Merge pull request #16 from geky/versioning 5a38d00 Added deploy step in Travis to push new version as tags 035552a Add version info for software library and on-disk structures 997c2e5 Fixed incorrect reliance on errno in emubd d88f0ac Added lfs_file_truncate 2ad435e Added files test to littlefs-fuse tests in Travis 1fb6a19 Reduced ctz traverse runtime by 2x db88727 Added error code LFS_ERR_NOTEMPTY c2fab8f Added asserts on geometry and updated config documentation 472ccc4 Fixed file truncation without writes aea3d3d Fixed positive seek bounds checking be22d34 Updated links to Mbed OS 425aa3c Fixed issue with immediate exhaustion and small unaligned storage git-subtree-dir: features/filesystem/littlefs/littlefs git-subtree-split: b2124a5ae54717737f6d96cb24e6f31e02158d1e --- .gitignore | 9 ++ .travis.yml | 239 ++++++++++++++++++++++++++---- DESIGN.md | 88 +++++------ Makefile | 20 +-- README.md | 27 ++-- SPEC.md | 12 +- emubd/lfs_emubd.c | 4 +- lfs.c | 325 +++++++++++++++++++++++++++++------------ lfs.h | 55 +++++-- lfs_util.h | 134 +++++++++++++++-- tests/template.fmt | 11 +- tests/test.py | 22 ++- tests/test_alloc.sh | 34 +++++ tests/test_dirs.sh | 10 +- tests/test_files.sh | 29 +++- tests/test_seek.sh | 16 ++ tests/test_truncate.sh | 158 ++++++++++++++++++++ 17 files changed, 949 insertions(+), 244 deletions(-) create mode 100644 .gitignore create mode 100755 tests/test_truncate.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..36f92cd81cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Compilation output +*.o +*.d +*.a + +# Testing things +blocks/ +lfs +test.c diff --git a/.travis.yml b/.travis.yml index d673c159ab8..2c6139fefce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,48 +1,223 @@ +# Environment variables +env: + global: + - CFLAGS=-Werror + +# Common test script script: - # make sure example can at least compile - - sed -n '/``` c/,/```/{/```/d; p;}' README.md > test.c && - CFLAGS=' + # make sure example can at least compile + - sed -n '/``` c/,/```/{/```/d; p;}' README.md > test.c && + make all CFLAGS+=" -Duser_provided_block_device_read=NULL -Duser_provided_block_device_prog=NULL -Duser_provided_block_device_erase=NULL -Duser_provided_block_device_sync=NULL - -include stdio.h -Werror' make all size + -include stdio.h" + + # run tests + - make test QUIET=1 + + # run tests with a few different configurations + - make test QUIET=1 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 clean test QUIET=1 CFLAGS+="-DLFS_NO_INTRINSICS" + + # compile and find the code size with the smallest configuration + - make clean size + OBJ="$(ls lfs*.o | tr '\n' ' ')" + CFLAGS+="-DLFS_NO{ASSERT,DEBUG,WARN,ERROR}" + | tee sizes + + # update status if we succeeded, compare with master if possible + - | + if [ "$TRAVIS_TEST_RESULT" -eq 0 ] + then + CURR=$(tail -n1 sizes | awk '{print $1}') + PREV=$(curl https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/master \ + | jq -re "select(.sha != \"$TRAVIS_COMMIT\") + | .statuses[] | select(.context == \"$STAGE/$NAME\").description + | capture(\"code size is (?[0-9]+)\").size" \ + || echo 0) - # run tests - - make test QUIET=1 + STATUS="Passed, code size is ${CURR}B" + if [ "$PREV" -ne 0 ] + then + STATUS="$STATUS ($(python -c "print '%+.2f' % (100*($CURR-$PREV)/$PREV.0)")%)" + fi + fi - # run tests with a few different configurations - - 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" make test QUIET=1 - - CFLAGS="-DLFS_LOOKAHEAD=2048" make test QUIET=1 +# CI matrix +jobs: + include: + # native testing + - stage: test + env: + - STAGE=test + - NAME=littlefs-x86 + + # cross-compile with ARM (thumb mode) + - stage: test + env: + - STAGE=test + - NAME=littlefs-arm + - CC="arm-linux-gnueabi-gcc --static -mthumb" + - EXEC="qemu-arm" + install: + - sudo apt-get install gcc-arm-linux-gnueabi qemu-user + - arm-linux-gnueabi-gcc --version + - qemu-arm -version + + # cross-compile with PowerPC + - stage: test + env: + - STAGE=test + - NAME=littlefs-powerpc + - CC="powerpc-linux-gnu-gcc --static" + - EXEC="qemu-ppc" + install: + - sudo apt-get install gcc-powerpc-linux-gnu qemu-user + - powerpc-linux-gnu-gcc --version + - qemu-ppc -version + + # cross-compile with MIPS + - stage: test + env: + - STAGE=test + - NAME=littlefs-mips + - CC="mips-linux-gnu-gcc --static" + - EXEC="qemu-mips" + install: + - sudo add-apt-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial main universe" + - sudo apt-get -qq update + - sudo apt-get install gcc-mips-linux-gnu qemu-user + - mips-linux-gnu-gcc --version + - qemu-mips -version # self-host with littlefs-fuse for fuzz test - - make -C littlefs-fuse + - stage: test + env: + - STAGE=test + - NAME=littlefs-fuse + install: + - sudo apt-get install libfuse-dev + - git clone --depth 1 https://github.com/geky/littlefs-fuse + - fusermount -V + - gcc --version + before_script: + # setup disk for littlefs-fuse + - rm -rf littlefs-fuse/littlefs/* + - cp -r $(git ls-tree --name-only HEAD) littlefs-fuse/littlefs + + - mkdir mount + - sudo chmod a+rw /dev/loop0 + - dd if=/dev/zero bs=512 count=2048 of=disk + - losetup /dev/loop0 disk + script: + # self-host test + - make -C littlefs-fuse + + - littlefs-fuse/lfs --format /dev/loop0 + - littlefs-fuse/lfs /dev/loop0 mount - - littlefs-fuse/lfs --format /dev/loop0 - - littlefs-fuse/lfs /dev/loop0 mount + - ls mount + - mkdir mount/littlefs + - cp -r $(git ls-tree --name-only HEAD) mount/littlefs + - cd mount/littlefs + - ls + - make -B test_dirs test_files QUIET=1 - - ls mount - - mkdir mount/littlefs - - cp -r $(git ls-tree --name-only HEAD) mount/littlefs - - cd mount/littlefs - - ls - - make -B test_dirs QUIET=1 + # Automatically update releases + - stage: deploy + env: + - STAGE=deploy + - NAME=deploy + script: + # Update tag for version defined in lfs.h + - LFS_VERSION=$(grep -ox '#define LFS_VERSION .*' lfs.h | cut -d ' ' -f3) + - LFS_VERSION_MAJOR=$((0xffff & ($LFS_VERSION >> 16))) + - LFS_VERSION_MINOR=$((0xffff & ($LFS_VERSION >> 0))) + - LFS_VERSION="v$LFS_VERSION_MAJOR.$LFS_VERSION_MINOR" + - echo "littlefs version $LFS_VERSION" + - | + curl -u $GEKY_BOT_RELEASES -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/git/refs \ + -d "{ + \"ref\": \"refs/tags/$LFS_VERSION\", + \"sha\": \"$TRAVIS_COMMIT\" + }" + - | + curl -f -u $GEKY_BOT_RELEASES -X PATCH \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/git/refs/tags/$LFS_VERSION \ + -d "{ + \"sha\": \"$TRAVIS_COMMIT\" + }" + # Create release notes from commits + - LFS_PREV_VERSION="v$LFS_VERSION_MAJOR.$(($LFS_VERSION_MINOR-1))" + - | + if [ $(git tag -l "$LFS_PREV_VERSION") ] + then + curl -u $GEKY_BOT_RELEASES -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases \ + -d "{ + \"tag_name\": \"$LFS_VERSION\", + \"name\": \"$LFS_VERSION\" + }" + RELEASE=$( + curl -f https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases/tags/$LFS_VERSION + ) + CHANGES=$( + git log --oneline $LFS_PREV_VERSION.. --grep='^Merge' --invert-grep + ) + curl -f -u $GEKY_BOT_RELEASES -X PATCH \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases/$( + jq -r '.id' <<< "$RELEASE" + ) \ + -d "$( + jq -s '{ + "body": ((.[0] // "" | sub("(?<=\n)#+ Changes.*"; ""; "mi")) + + "### Changes\n\n" + .[1]) + }' <(jq '.body' <<< "$RELEASE") <(jq -sR '.' <<< "$CHANGES") + )" + fi +# Manage statuses before_install: - - fusermount -V - - gcc --version + - | + curl -u $GEKY_BOT_STATUSES -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + -d "{ + \"context\": \"$STAGE/$NAME\", + \"state\": \"pending\", + \"description\": \"${STATUS:-In progress}\", + \"target_url\": \"https://travis-ci.org/$TRAVIS_REPO_SLUG/jobs/$TRAVIS_JOB_ID\" + }" -install: - - sudo apt-get install libfuse-dev - - git clone --depth 1 https://github.com/geky/littlefs-fuse +after_failure: + - | + curl -u $GEKY_BOT_STATUSES -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + -d "{ + \"context\": \"$STAGE/$NAME\", + \"state\": \"failure\", + \"description\": \"${STATUS:-Failed}\", + \"target_url\": \"https://travis-ci.org/$TRAVIS_REPO_SLUG/jobs/$TRAVIS_JOB_ID\" + }" -before_script: - - rm -rf littlefs-fuse/littlefs/* - - cp -r $(git ls-tree --name-only HEAD) littlefs-fuse/littlefs +after_success: + - | + curl -u $GEKY_BOT_STATUSES -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + -d "{ + \"context\": \"$STAGE/$NAME\", + \"state\": \"success\", + \"description\": \"${STATUS:-Passed}\", + \"target_url\": \"https://travis-ci.org/$TRAVIS_REPO_SLUG/jobs/$TRAVIS_JOB_ID\" + }" - - mkdir mount - - sudo chmod a+rw /dev/loop0 - - dd if=/dev/zero bs=512 count=2048 of=disk - - losetup /dev/loop0 disk +# Job control +stages: + - name: test + - name: deploy + if: branch = master diff --git a/DESIGN.md b/DESIGN.md index f2a8498443a..3afb0a20542 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -27,16 +27,17 @@ cheap, and can be very granular. For NOR flash specifically, byte-level programs are quite common. Erasing, however, requires an expensive operation that forces the state of large blocks of memory to reset in a destructive reaction that gives flash its name. The [Wikipedia entry](https://en.wikipedia.org/wiki/Flash_memory) -has more information if you are interesting in how this works. +has more information if you are interested in how this works. This leaves us with an interesting set of limitations that can be simplified to three strong requirements: 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. + 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 at any given time. Despite this state of things, there are very few embedded filesystems that handle power loss in a reasonable manner, and most can become corrupted if @@ -52,7 +53,8 @@ to three strong requirements: which stores a file allocation table (FAT) at a specific offset from the beginning of disk. Every block allocation will update this table, and after 100,000 updates, the block will likely go bad, rendering the filesystem - unusable even if there are many more erase cycles available on the storage. + unusable even if there are many more erase cycles available on the storage + as a whole. 3. **Bounded RAM/ROM** - Even with the design difficulties presented by the previous two limitations, we have already seen several flash filesystems @@ -72,7 +74,7 @@ to three strong requirements: ## Existing designs? -There are of course, many different existing filesystem. Heres a very rough +There are of course, many different existing filesystem. Here is a very rough summary of the general ideas behind some of them. Most of the existing filesystems fall into the one big category of filesystem @@ -80,21 +82,21 @@ 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 make as much sense on recent storage types. For instance, on flash, write -locality is not important and can actually increase wear destructively. +locality is not important and can actually increase wear. One of the most popular designs for flash filesystems is called the [logging filesystem](https://en.wikipedia.org/wiki/Log-structured_file_system). The flash filesystems [jffs](https://en.wikipedia.org/wiki/JFFS) -and [yaffs](https://en.wikipedia.org/wiki/YAFFS) are good examples. In -logging filesystem, data is not store in a data structure on disk, but instead +and [yaffs](https://en.wikipedia.org/wiki/YAFFS) are good examples. In a +logging filesystem, data is not stored in a data structure on disk, but instead the changes to the files are stored on disk. This has several neat advantages, -such as the fact that the data is written in a cyclic log format naturally +such as the fact that the data is written in a cyclic log format and naturally wear levels as a side effect. And, with a bit of error detection, the entire filesystem can easily be designed to be resilient to power loss. The -journalling component of most modern day filesystems is actually a reduced +journaling component of most modern day filesystems is actually a reduced form of a logging filesystem. However, logging filesystems have a difficulty 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 +caching large parts of the filesystem in RAM, a strategy that is inappropriate for embedded systems. Another interesting filesystem design technique is that of [copy-on-write (COW)](https://en.wikipedia.org/wiki/Copy-on-write). @@ -107,14 +109,14 @@ where the COW data structures are synchronized. ## Metadata pairs The core piece of technology that provides the backbone for the littlefs is -the concept of metadata pairs. The key idea here, is that any metadata that +the concept of metadata pairs. The key idea here is that any metadata that needs to be updated atomically is stored on a pair of blocks tagged with a revision count and checksum. Every update alternates between these two pairs, so that at any time there is always a backup containing the previous state of the metadata. Consider a small example where each metadata pair has a revision count, -a number as data, and the xor of the block as a quick checksum. If +a number as data, and the XOR of the block as a quick checksum. If we update the data to a value of 9, and then to a value of 5, here is what the pair of blocks may look like after each update: ``` @@ -130,7 +132,7 @@ what the pair of blocks may look like after each update: After each update, we can find the most up to date value of data by looking at the revision count. -Now consider what the blocks may look like if we suddenly loss power while +Now consider what the blocks may look like if we suddenly lose power while changing the value of data to 5: ``` block 1 block 2 block 1 block 2 block 1 block 2 @@ -149,7 +151,7 @@ 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 32 bit 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. @@ -161,7 +163,7 @@ requires two blocks for each block of data. I'm sure users would be very unhappy if their storage was suddenly cut in half! Instead of storing everything in these metadata blocks, the littlefs uses a COW data structure for files which is in turn pointed to by a metadata block. When -we update a file, we create a copies of any blocks that are modified until +we update a file, we create copies of any blocks that are modified until the metadata blocks are updated with the new copy. Once the metadata block points to the new copy, we deallocate the old blocks that are no longer in use. @@ -184,7 +186,7 @@ Here is what updating a one-block file may look like: update data in file update metadata pair ``` -It doesn't matter if we lose power while writing block 5 with the new data, +It doesn't matter if we lose power while writing new data to block 5, since the old data remains unmodified in block 4. This example also highlights how the atomic updates of the metadata blocks provide a synchronization barrier for the rest of the littlefs. @@ -206,7 +208,7 @@ files in filesystems. Of these, the littlefs uses a rather unique [COW](https:// data structure that allows the filesystem to reuse unmodified parts of the file without additional metadata pairs. -First lets consider storing files in a simple linked-list. What happens when +First lets consider storing files in a simple linked-list. What happens when we append a block? We have to change the last block in the linked-list to point to this new block, which means we have to copy out the last block, and change the second-to-last block, and then the third-to-last, and so on until we've @@ -240,8 +242,8 @@ Exhibit B: A backwards linked-list ``` However, a backwards linked-list does come with a rather glaring problem. -Iterating over a file _in order_ has a runtime of O(n^2). Gah! A quadratic -runtime to just _read_ a file? That's awful. Keep in mind reading files are +Iterating over a file _in order_ has a runtime cost of O(n^2). Gah! A quadratic +runtime to just _read_ a file? That's awful. Keep in mind reading files is usually the most common filesystem operation. To avoid this problem, the littlefs uses a multilayered linked-list. For @@ -266,7 +268,7 @@ Exhibit C: A backwards CTZ skip-list ``` The additional pointers allow us to navigate the data-structure on disk -much more efficiently than in a single linked-list. +much more efficiently than in a singly 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: @@ -289,15 +291,15 @@ The path to data block 0 is even more quick, requiring only two jumps: 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). +the search space for the block in half. This gives us a runtime of O(log n). 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 +backwards, which puts the runtime at O(2 log n) = O(log n). 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. 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). +a runtime of O(1), and can be read with a worst case runtime of O(n log n). Given that the the runtime is also divided by the amount of data we can store in a block, this is pretty reasonable. @@ -362,7 +364,7 @@ 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, +to O(n^2 log n). 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. @@ -379,11 +381,11 @@ unintuitive property: ![mindblown](https://latex.codecogs.com/svg.latex?%5Csum_i%5En%5Cleft%28%5Ctext%7Bctz%7D%28i%29+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 +ctz(x) = the number of trailing bits that are 0 in x +popcount(x) = the number of bits that are 1 in x 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 +are related by this property. But if we start to dissect 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. @@ -410,8 +412,7 @@ 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). +Now we can solve for both the block index and offset from the file size in O(1). Here is what it might look like to update a file stored with a CTZ skip-list: ``` @@ -500,16 +501,17 @@ scanned to find the most recent free list, but once the list was found the state of all free blocks becomes known. However, this approach had several issues: + - There was a lot of nuanced logic for adding blocks to the free list without modifying the blocks, since the blocks remain active until the metadata is updated. -- The free list had to support both additions and removals in fifo order while +- The free list had to support both additions and removals in FIFO order while minimizing block erases. - The free list had to handle the case where the file system completely ran out of blocks and may no longer be able to add blocks to the free list. - If we used a revision count to track the most recently updated free list, metadata blocks that were left unmodified were ticking time bombs that would - cause the system to go haywire if the revision count overflowed + cause the system to go haywire if the revision count overflowed. - Every single metadata block wasted space to store these free list references. Actually, to simplify, this approach had one massive glaring issue: complexity. @@ -539,7 +541,7 @@ would have an abhorrent runtime. So the littlefs compromises. It doesn't store a bitmap the size of the storage, but it does store a little bit-vector that contains a fixed set lookahead for block allocations. During a block allocation, the lookahead vector is -checked for any free blocks, if there are none, the lookahead region jumps +checked for any free blocks. If there are none, the lookahead region jumps forward and the entire filesystem is scanned for free blocks. Here's what it might look like to allocate 4 blocks on a decently busy @@ -622,7 +624,7 @@ So, as a solution, the littlefs adopted a sort of threaded tree. Each directory not only contains pointers to all of its children, but also a pointer to the next directory. These pointers create a linked-list that is threaded through all of the directories in the filesystem. Since we -only use this linked list to check for existance, the order doesn't actually +only use this linked list to check for existence, the order doesn't actually matter. As an added plus, we can repurpose the pointer for the individual directory linked-lists and avoid using any additional space. @@ -773,7 +775,7 @@ 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. Note that the deorphan step never needs to run in a readonly +prematurely. Note that the deorphan step never needs to run in a read-only filesystem. ## The move problem @@ -883,7 +885,7 @@ 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 +So what littlefs does is inelegantly 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. @@ -979,7 +981,7 @@ if it exists elsewhere in the filesystem. So now that we have all of the pieces of a filesystem, we can look at a more subtle attribute of embedded storage: The wear down of flash blocks. -The first concern for the littlefs, is that prefectly valid blocks can suddenly +The first concern for the littlefs, is that perfectly valid blocks can suddenly become unusable. As a nice side-effect of using a COW data-structure for files, we can simply move on to a different block when a file write fails. All modifications to files are performed in copies, so we will only replace the @@ -1151,7 +1153,7 @@ develops errors and needs to be moved. ## Wear leveling -The second concern for the littlefs, is that blocks in the filesystem may wear +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's common to have files that were written once and left unmodified, wasting the potential @@ -1171,7 +1173,7 @@ of 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 +of files, littlefs could provide your usual implementation of dynamic wear leveling. However, the littlefs does not. This is for a few reasons. Most notably, even @@ -1210,9 +1212,9 @@ So, to summarize: metadata block is active 4. Directory blocks contain either references to other directories or files 5. Files are represented by copy-on-write CTZ skip-lists which support O(1) - append and O(nlogn) reading + append and O(n log n) 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 + fixed-size lookahead region that is stored in a bit-vector 7. To facilitate scanning the filesystem, all directories are part of a linked-list that is threaded through the entire filesystem 8. If a block develops an error, the littlefs allocates a new block, and diff --git a/Makefile b/Makefile index cf978e79293..ee717936bd4 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ TARGET = lfs -CC = gcc -AR = ar -SIZE = size +CC ?= gcc +AR ?= ar +SIZE ?= size SRC += $(wildcard *.c emubd/*.c) OBJ := $(SRC:.c=.o) @@ -14,15 +14,15 @@ TEST := $(patsubst tests/%.sh,%,$(wildcard tests/test_*)) SHELL = /bin/bash -o pipefail ifdef DEBUG -CFLAGS += -O0 -g3 +override CFLAGS += -O0 -g3 else -CFLAGS += -Os +override CFLAGS += -Os endif ifdef WORD -CFLAGS += -m$(WORD) +override CFLAGS += -m$(WORD) endif -CFLAGS += -I. -CFLAGS += -std=c99 -Wall -pedantic +override CFLAGS += -I. +override CFLAGS += -std=c99 -Wall -pedantic all: $(TARGET) @@ -33,11 +33,11 @@ size: $(OBJ) $(SIZE) -t $^ .SUFFIXES: -test: test_format test_dirs test_files test_seek test_parallel \ +test: test_format test_dirs test_files test_seek test_truncate test_parallel \ test_alloc test_paths test_orphan test_move test_corrupt test_%: tests/test_%.sh ifdef QUIET - ./$< | sed -n '/^[-=]/p' + @./$< | sed -n '/^[-=]/p' else ./$< endif diff --git a/README.md b/README.md index d02139c443d..9c06a266832 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ of memory. Recursion is avoided and dynamic memory is limited to configurable buffers that can be provided statically. **Power-loss resilient** - The littlefs is designed for systems that may have -random power failures. The littlefs has strong copy-on-write guaruntees and +random power failures. The littlefs has strong copy-on-write guarantees and storage on disk is always kept in a valid state. **Wear leveling** - Since the most common form of embedded storage is erodible @@ -88,7 +88,7 @@ int main(void) { ## Usage Detailed documentation (or at least as much detail as is currently available) -can be cound in the comments in [lfs.h](lfs.h). +can be found in the comments in [lfs.h](lfs.h). As you may have noticed, littlefs takes in a configuration structure that defines how the filesystem operates. The configuration struct provides the @@ -101,12 +101,12 @@ to the user to allocate, allowing multiple filesystems to be in use 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 +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. -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 +All POSIX operations, such as remove and rename, are atomic, even in event +of power-loss. Additionally, no file updates are actually committed to the filesystem until sync or close is called on the file. ## Other notes @@ -116,7 +116,7 @@ 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 current implementation of littlefs doesn't -really do anything to insure that the data written to disk is machine portable. +really do anything to ensure 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. @@ -131,9 +131,9 @@ with all the nitty-gritty details. Can be useful for developing tooling. ## Testing -The littlefs comes with a test suite designed to run on a pc using the +The littlefs comes with a test suite designed to run on a PC using the [emulated block device](emubd/lfs_emubd.h) found in the emubd directory. -The tests assume a linux environment and can be started with make: +The tests assume a Linux environment and can be started with make: ``` bash make test @@ -141,13 +141,14 @@ make test ## Related projects -[mbed-littlefs](https://github.com/armmbed/mbed-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 -mbed-littlefs provides the mbed wrapper for littlefs. +[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 +wrapper for littlefs. The project allows you to mount littlefs directly on a Linux machine. Can be useful for debugging littlefs if you have an SD card handy. diff --git a/SPEC.md b/SPEC.md index b80892ec88c..2a1f9eca8b0 100644 --- a/SPEC.md +++ b/SPEC.md @@ -46,7 +46,7 @@ Here's the layout of metadata blocks on disk: | 0x04 | 32 bits | dir size | | 0x08 | 64 bits | tail pointer | | 0x10 | size-16 bytes | dir entries | -| 0x00+s | 32 bits | crc | +| 0x00+s | 32 bits | CRC | **Revision count** - Incremented every update, only the uncorrupted metadata-block with the most recent revision count contains the valid metadata. @@ -75,7 +75,7 @@ Here's an example of a simple directory stored on disk: (32 bits) revision count = 10 (0x0000000a) (32 bits) dir size = 154 bytes, end of dir (0x0000009a) (64 bits) tail pointer = 37, 36 (0x00000025, 0x00000024) -(32 bits) crc = 0xc86e3106 +(32 bits) CRC = 0xc86e3106 00000000: 0a 00 00 00 9a 00 00 00 25 00 00 00 24 00 00 00 ........%...$... 00000010: 22 08 00 03 05 00 00 00 04 00 00 00 74 65 61 22 "...........tea" @@ -138,12 +138,12 @@ 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. **Attribute length** - Length of system-specific attributes in bytes. Since -attributes are system specific, there is not much garuntee on the values in +attributes are system specific, there is not much guarantee on the values in this section, and systems are expected to work even when it is empty. See the [attributes](#entry-attributes) section for more details. -**Name length** - Length of the entry name. Entry names are stored as utf8, -although most systems will probably only support ascii. Entry names can not +**Name length** - Length of the entry name. Entry names are stored as UTF8, +although most systems will probably only support ASCII. Entry names can not contain '/' and can not be '.' or '..' as these are a part of the syntax of filesystem paths. @@ -222,7 +222,7 @@ Here's an example of a complete superblock: (32 bits) block count = 1024 blocks (0x00000400) (32 bits) version = 1.1 (0x00010001) (8 bytes) magic string = littlefs -(32 bits) crc = 0xc50b74fa +(32 bits) CRC = 0xc50b74fa 00000000: 03 00 00 00 34 00 00 00 03 00 00 00 02 00 00 00 ....4........... 00000010: 2e 14 00 08 03 00 00 00 02 00 00 00 00 02 00 00 ................ diff --git a/emubd/lfs_emubd.c b/emubd/lfs_emubd.c index b87d6deba03..b1595963dd9 100644 --- a/emubd/lfs_emubd.c +++ b/emubd/lfs_emubd.c @@ -190,13 +190,13 @@ int lfs_emubd_erase(const struct lfs_config *cfg, lfs_block_t block) { } if (!err && S_ISREG(st.st_mode) && (S_IWUSR & st.st_mode)) { - int err = unlink(emu->path); + err = unlink(emu->path); if (err) { return -errno; } } - if (errno == ENOENT || (S_ISREG(st.st_mode) && (S_IWUSR & st.st_mode))) { + if (err || (S_ISREG(st.st_mode) && (S_IWUSR & st.st_mode))) { FILE *f = fopen(emu->path, "w"); if (!f) { return -errno; diff --git a/lfs.c b/lfs.c index ea116d27c94..a0e0b0f50c5 100644 --- a/lfs.c +++ b/lfs.c @@ -18,17 +18,13 @@ #include "lfs.h" #include "lfs_util.h" -#include -#include -#include - /// Caching block device operations /// static int lfs_cache_read(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { uint8_t *data = buffer; - assert(block < lfs->cfg->block_count); + LFS_ASSERT(block < lfs->cfg->block_count); while (size > 0) { if (pcache && block == pcache->block && off >= pcache->off && @@ -153,7 +149,7 @@ static int lfs_cache_prog(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { const uint8_t *data = buffer; - assert(block < lfs->cfg->block_count); + LFS_ASSERT(block < lfs->cfg->block_count); while (size > 0) { if (block == pcache->block && off >= pcache->off && @@ -180,7 +176,7 @@ static int lfs_cache_prog(lfs_t *lfs, lfs_cache_t *pcache, // pcache must have been flushed, either by programming and // entire block or manually flushing the pcache - assert(pcache->block == 0xffffffff); + LFS_ASSERT(pcache->block == 0xffffffff); if (off % lfs->cfg->prog_size == 0 && size >= lfs->cfg->prog_size) { @@ -278,7 +274,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->cfg->lookahead) { + if (off < lfs->free.size) { lfs->free.buffer[off / 32] |= 1U << (off % 32); } @@ -287,18 +283,7 @@ static int lfs_alloc_lookahead(void *p, lfs_block_t block) { static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { while (true) { - while (true) { - // check if we have looked at all blocks since last ack - if (lfs->free.begin + lfs->free.off == lfs->free.end) { - LFS_WARN("No more free space %d", lfs->free.end); - return LFS_ERR_NOSPC; - } - - if (lfs->free.off >= lfs_min( - lfs->cfg->lookahead, lfs->cfg->block_count)) { - break; - } - + while (lfs->free.off != lfs->free.size) { lfs_block_t off = lfs->free.off; lfs->free.off += 1; @@ -309,7 +294,15 @@ static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { } } - lfs->free.begin += lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count); + // check if we have looked at all blocks since last ack + if (lfs->free.off == lfs->free.ack - lfs->free.begin) { + LFS_WARN("No more free space %d", lfs->free.off + lfs->free.begin); + return LFS_ERR_NOSPC; + } + + lfs->free.begin += lfs->free.size; + lfs->free.size = lfs_min(lfs->cfg->lookahead, + lfs->free.ack - lfs->free.begin); lfs->free.off = 0; // find mask of free blocks from tree @@ -322,7 +315,49 @@ static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { } static void lfs_alloc_ack(lfs_t *lfs) { - lfs->free.end = lfs->free.begin + lfs->free.off + lfs->cfg->block_count; + lfs->free.ack = lfs->free.off-1 + lfs->free.begin + lfs->cfg->block_count; +} + + +/// Endian swapping functions /// +static void lfs_dir_fromle32(struct lfs_disk_dir *d) { + d->rev = lfs_fromle32(d->rev); + d->size = lfs_fromle32(d->size); + d->tail[0] = lfs_fromle32(d->tail[0]); + d->tail[1] = lfs_fromle32(d->tail[1]); +} + +static void lfs_dir_tole32(struct lfs_disk_dir *d) { + d->rev = lfs_tole32(d->rev); + d->size = lfs_tole32(d->size); + d->tail[0] = lfs_tole32(d->tail[0]); + d->tail[1] = lfs_tole32(d->tail[1]); +} + +static void lfs_entry_fromle32(struct lfs_disk_entry *d) { + d->u.dir[0] = lfs_fromle32(d->u.dir[0]); + d->u.dir[1] = lfs_fromle32(d->u.dir[1]); +} + +static void lfs_entry_tole32(struct lfs_disk_entry *d) { + d->u.dir[0] = lfs_tole32(d->u.dir[0]); + d->u.dir[1] = lfs_tole32(d->u.dir[1]); +} + +static void lfs_superblock_fromle32(struct lfs_disk_superblock *d) { + d->root[0] = lfs_fromle32(d->root[0]); + d->root[1] = lfs_fromle32(d->root[1]); + d->block_size = lfs_fromle32(d->block_size); + d->block_count = lfs_fromle32(d->block_count); + d->version = lfs_fromle32(d->version); +} + +static void lfs_superblock_tole32(struct lfs_disk_superblock *d) { + d->root[0] = lfs_tole32(d->root[0]); + d->root[1] = lfs_tole32(d->root[1]); + d->block_size = lfs_tole32(d->block_size); + d->block_count = lfs_tole32(d->block_count); + d->version = lfs_tole32(d->version); } @@ -367,6 +402,7 @@ static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) { // rather than clobbering one of the blocks we just pretend // the revision may be valid int err = lfs_bd_read(lfs, dir->pair[0], 0, &dir->d.rev, 4); + dir->d.rev = lfs_fromle32(dir->d.rev); if (err) { return err; } @@ -392,6 +428,7 @@ static int lfs_dir_fetch(lfs_t *lfs, for (int i = 0; i < 2; i++) { struct lfs_disk_dir test; int err = lfs_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); + lfs_dir_fromle32(&test); if (err) { return err; } @@ -406,7 +443,9 @@ static int lfs_dir_fetch(lfs_t *lfs, } uint32_t crc = 0xffffffff; + lfs_dir_tole32(&test); lfs_crc(&crc, &test, sizeof(test)); + lfs_dir_fromle32(&test); err = lfs_bd_crc(lfs, tpair[i], sizeof(test), (0x7fffffff & test.size) - sizeof(test), &crc); if (err) { @@ -466,8 +505,10 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, } uint32_t crc = 0xffffffff; + lfs_dir_tole32(&dir->d); lfs_crc(&crc, &dir->d, sizeof(dir->d)); err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d)); + lfs_dir_fromle32(&dir->d); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; @@ -481,7 +522,7 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, 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], + err = lfs_bd_prog(lfs, dir->pair[0], newoff, regions[i].newdata, regions[i].newlen); if (err) { if (err == LFS_ERR_CORRUPT) { @@ -495,7 +536,7 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, i += 1; } else { uint8_t data; - int err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1); + err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1); if (err) { return err; } @@ -514,7 +555,9 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, } } + crc = lfs_tole32(crc); err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4); + crc = lfs_fromle32(crc); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; @@ -587,11 +630,14 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, } static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir, - const lfs_entry_t *entry, const void *data) { - return lfs_dir_commit(lfs, dir, (struct lfs_region[]){ + lfs_entry_t *entry, const void *data) { + lfs_entry_tole32(&entry->d); + int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){ {entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)}, {entry->off+sizeof(entry->d), entry->d.nlen, data, entry->d.nlen} }, data ? 2 : 1); + lfs_entry_fromle32(&entry->d); + return err; } static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, @@ -600,10 +646,14 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, while (true) { if (dir->d.size + lfs_entry_size(entry) <= lfs->cfg->block_size) { entry->off = dir->d.size - 4; - return lfs_dir_commit(lfs, dir, (struct lfs_region[]){ + + lfs_entry_tole32(&entry->d); + int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){ {entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen} }, 2); + lfs_entry_fromle32(&entry->d); + return err; } // we need to allocate a new dir block @@ -617,10 +667,12 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, newdir.d.tail[0] = dir->d.tail[0]; newdir.d.tail[1] = dir->d.tail[1]; entry->off = newdir.d.size - 4; + lfs_entry_tole32(&entry->d); err = lfs_dir_commit(lfs, &newdir, (struct lfs_region[]){ {entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen} }, 2); + lfs_entry_fromle32(&entry->d); if (err) { return err; } @@ -706,6 +758,7 @@ static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { int err = lfs_bd_read(lfs, dir->pair[0], dir->off, &entry->d, sizeof(entry->d)); + lfs_entry_fromle32(&entry->d); if (err) { return err; } @@ -1005,7 +1058,7 @@ int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { return LFS_ERR_INVAL; } - int err = lfs_dir_fetch(lfs, dir, dir->d.tail); + err = lfs_dir_fetch(lfs, dir, dir->d.tail); if (err) { return err; } @@ -1016,6 +1069,7 @@ int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { } lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir) { + (void)lfs; return dir->pos; } @@ -1067,11 +1121,12 @@ static int lfs_ctz_find(lfs_t *lfs, lfs_ctz(current)); int err = lfs_cache_read(lfs, rcache, pcache, head, 4*skip, &head, 4); + head = lfs_fromle32(head); if (err) { return err; } - assert(head >= 2 && head <= lfs->cfg->block_count); + LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); current -= 1 << skip; } @@ -1091,7 +1146,7 @@ static int lfs_ctz_extend(lfs_t *lfs, if (err) { return err; } - assert(nblock >= 2 && nblock <= lfs->cfg->block_count); + LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count); if (true) { err = lfs_bd_erase(lfs, nblock); @@ -1116,7 +1171,7 @@ static int lfs_ctz_extend(lfs_t *lfs, 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, + err = lfs_cache_read(lfs, rcache, NULL, head, i, &data, 1); if (err) { return err; @@ -1142,8 +1197,10 @@ static int lfs_ctz_extend(lfs_t *lfs, 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, + head = lfs_tole32(head); + err = lfs_cache_prog(lfs, pcache, rcache, nblock, 4*i, &head, 4); + head = lfs_fromle32(head); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; @@ -1154,12 +1211,13 @@ static int lfs_ctz_extend(lfs_t *lfs, if (i != skips-1) { err = lfs_cache_read(lfs, rcache, NULL, head, 4*i, &head, 4); + head = lfs_fromle32(head); if (err) { return err; } } - assert(head >= 2 && head <= lfs->cfg->block_count); + LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); } *block = nblock; @@ -1195,12 +1253,24 @@ static int lfs_ctz_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); + heads[0] = lfs_fromle32(heads[0]); + heads[1] = lfs_fromle32(heads[1]); if (err) { return err; } - index -= 1; + for (int i = 0; i < count-1; i++) { + err = cb(data, heads[i]); + if (err) { + return err; + } + } + + head = heads[count-1]; + index -= count; } } @@ -1261,6 +1331,9 @@ int lfs_file_open(lfs_t *lfs, lfs_file_t *file, file->pos = 0; if (flags & LFS_O_TRUNC) { + if (file->size != 0) { + file->flags |= LFS_F_DIRTY; + } file->head = 0xffffffff; file->size = 0; } @@ -1270,12 +1343,12 @@ int lfs_file_open(lfs_t *lfs, lfs_file_t *file, if (lfs->cfg->file_buffer) { file->cache.buffer = lfs->cfg->file_buffer; } else if ((file->flags & 3) == LFS_O_RDONLY) { - file->cache.buffer = malloc(lfs->cfg->read_size); + file->cache.buffer = lfs_malloc(lfs->cfg->read_size); if (!file->cache.buffer) { return LFS_ERR_NOMEM; } } else { - file->cache.buffer = malloc(lfs->cfg->prog_size); + file->cache.buffer = lfs_malloc(lfs->cfg->prog_size); if (!file->cache.buffer) { return LFS_ERR_NOMEM; } @@ -1301,7 +1374,7 @@ int lfs_file_close(lfs_t *lfs, lfs_file_t *file) { // clean up memory if (!lfs->cfg->file_buffer) { - free(file->cache.buffer); + lfs_free(file->cache.buffer); } return err; @@ -1437,7 +1510,7 @@ int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) { !lfs_pairisnull(file->pair)) { // update dir entry lfs_dir_t cwd; - int err = lfs_dir_fetch(lfs, &cwd, file->pair); + err = lfs_dir_fetch(lfs, &cwd, file->pair); if (err) { return err; } @@ -1445,15 +1518,12 @@ int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) { lfs_entry_t entry = {.off = file->poff}; err = lfs_bd_read(lfs, cwd.pair[0], entry.off, &entry.d, sizeof(entry.d)); + lfs_entry_fromle32(&entry.d); if (err) { return err; } - if (entry.d.type != LFS_TYPE_REG) { - // sanity check valid entry - return LFS_ERR_INVAL; - } - + LFS_ASSERT(entry.d.type == LFS_TYPE_REG); entry.d.u.file.head = file->head; entry.d.u.file.size = file->size; @@ -1474,7 +1544,7 @@ lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, lfs_size_t nsize = size; if ((file->flags & 3) == LFS_O_WRONLY) { - return LFS_ERR_INVAL; + return LFS_ERR_BADF; } if (file->flags & LFS_F_WRITING) { @@ -1530,7 +1600,7 @@ lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, lfs_size_t nsize = size; if ((file->flags & 3) == LFS_O_RDONLY) { - return LFS_ERR_INVAL; + return LFS_ERR_BADF; } if (file->flags & LFS_F_READING) { @@ -1635,13 +1705,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 ((lfs_off_t)-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 ((lfs_off_t)-off > file->size) { + if (off < 0 && (lfs_off_t)-off > file->size) { return LFS_ERR_INVAL; } @@ -1651,7 +1721,60 @@ lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, return file->pos; } +int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { + if ((file->flags & 3) == LFS_O_RDONLY) { + return LFS_ERR_BADF; + } + + lfs_off_t oldsize = lfs_file_size(lfs, file); + if (size < oldsize) { + // need to flush since directly changing metadata + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // lookup new head in ctz skip list + err = lfs_ctz_find(lfs, &file->cache, NULL, + file->head, file->size, + size, &file->head, &(lfs_off_t){0}); + if (err) { + return err; + } + + file->size = size; + file->flags |= LFS_F_DIRTY; + } else if (size > oldsize) { + lfs_off_t pos = file->pos; + + // flush+seek if not already at end + if (file->pos != oldsize) { + int err = lfs_file_seek(lfs, file, 0, LFS_SEEK_END); + if (err < 0) { + return err; + } + } + + // fill with zeros + while (file->pos < size) { + lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { + return res; + } + } + + // restore pos + int err = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET); + if (err < 0) { + return err; + } + } + + return 0; +} + lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file) { + (void)lfs; return file->pos; } @@ -1665,11 +1788,16 @@ int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) { } lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) { - return lfs_max(file->pos, file->size); + (void)lfs; + if (file->flags & LFS_F_WRITING) { + return lfs_max(file->pos, file->size); + } else { + return file->size; + } } -/// General fs oprations /// +/// General fs operations /// int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) { // check for root, can only be something like '/././../.' if (strspn(path, "/.") == strlen(path)) { @@ -1733,11 +1861,11 @@ int lfs_remove(lfs_t *lfs, const char *path) { // must be empty before removal, checking size // without masking top bit checks for any case where // dir is not empty - int err = lfs_dir_fetch(lfs, &dir, entry.d.u.dir); + err = lfs_dir_fetch(lfs, &dir, entry.d.u.dir); if (err) { return err; } else if (dir.d.size != sizeof(dir.d)+4) { - return LFS_ERR_INVAL; + return LFS_ERR_NOTEMPTY; } } @@ -1754,11 +1882,11 @@ int lfs_remove(lfs_t *lfs, const char *path) { return res; } - assert(res); // must have pred + LFS_ASSERT(res); // must have pred cwd.d.tail[0] = dir.d.tail[0]; cwd.d.tail[1] = dir.d.tail[1]; - int err = lfs_dir_commit(lfs, &cwd, NULL, 0); + err = lfs_dir_commit(lfs, &cwd, NULL, 0); if (err) { return err; } @@ -1807,7 +1935,7 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { // must have same type if (prevexists && preventry.d.type != oldentry.d.type) { - return LFS_ERR_INVAL; + return LFS_ERR_ISDIR; } lfs_dir_t dir; @@ -1815,11 +1943,11 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { // must be empty before removal, checking size // without masking top bit checks for any case where // dir is not empty - int err = lfs_dir_fetch(lfs, &dir, preventry.d.u.dir); + err = lfs_dir_fetch(lfs, &dir, preventry.d.u.dir); if (err) { return err; } else if (dir.d.size != sizeof(dir.d)+4) { - return LFS_ERR_INVAL; + return LFS_ERR_NOTEMPTY; } } @@ -1842,12 +1970,12 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { newentry.d.nlen = strlen(newpath); if (prevexists) { - int err = lfs_dir_update(lfs, &newcwd, &newentry, newpath); + err = lfs_dir_update(lfs, &newcwd, &newentry, newpath); if (err) { return err; } } else { - int err = lfs_dir_append(lfs, &newcwd, &newentry, newpath); + err = lfs_dir_append(lfs, &newcwd, &newentry, newpath); if (err) { return err; } @@ -1871,11 +1999,11 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { return res; } - assert(res); // must have pred + LFS_ASSERT(res); // must have pred newcwd.d.tail[0] = dir.d.tail[0]; newcwd.d.tail[1] = dir.d.tail[1]; - int err = lfs_dir_commit(lfs, &newcwd, NULL, 0); + err = lfs_dir_commit(lfs, &newcwd, NULL, 0); if (err) { return err; } @@ -1894,7 +2022,7 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { if (lfs->cfg->read_buffer) { lfs->rcache.buffer = lfs->cfg->read_buffer; } else { - lfs->rcache.buffer = malloc(lfs->cfg->read_size); + lfs->rcache.buffer = lfs_malloc(lfs->cfg->read_size); if (!lfs->rcache.buffer) { return LFS_ERR_NOMEM; } @@ -1905,26 +2033,30 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { if (lfs->cfg->prog_buffer) { lfs->pcache.buffer = lfs->cfg->prog_buffer; } else { - lfs->pcache.buffer = malloc(lfs->cfg->prog_size); + lfs->pcache.buffer = lfs_malloc(lfs->cfg->prog_size); if (!lfs->pcache.buffer) { return LFS_ERR_NOMEM; } } // setup lookahead, round down to nearest 32-bits - assert(lfs->cfg->lookahead % 32 == 0); - assert(lfs->cfg->lookahead > 0); + LFS_ASSERT(lfs->cfg->lookahead % 32 == 0); + LFS_ASSERT(lfs->cfg->lookahead > 0); if (lfs->cfg->lookahead_buffer) { lfs->free.buffer = lfs->cfg->lookahead_buffer; } else { - lfs->free.buffer = malloc(lfs->cfg->lookahead/8); + lfs->free.buffer = lfs_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 + LFS_ASSERT(lfs->cfg->prog_size % lfs->cfg->read_size == 0); + LFS_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_ASSERT(4*lfs_npw2(0xffffffff / (lfs->cfg->block_size-2*4)) <= lfs->cfg->block_size); // setup default state @@ -1940,15 +2072,15 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { static int lfs_deinit(lfs_t *lfs) { // free allocated memory if (!lfs->cfg->read_buffer) { - free(lfs->rcache.buffer); + lfs_free(lfs->rcache.buffer); } if (!lfs->cfg->prog_buffer) { - free(lfs->pcache.buffer); + lfs_free(lfs->pcache.buffer); } if (!lfs->cfg->lookahead_buffer) { - free(lfs->free.buffer); + lfs_free(lfs->free.buffer); } return 0; @@ -1963,11 +2095,11 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { // create free lookahead memset(lfs->free.buffer, 0, lfs->cfg->lookahead/8); lfs->free.begin = 0; + lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count); lfs->free.off = 0; - lfs->free.end = lfs->free.begin + lfs->free.off + lfs->cfg->block_count; + lfs_alloc_ack(lfs); // create superblock dir - lfs_alloc_ack(lfs); lfs_dir_t superdir; err = lfs_dir_alloc(lfs, &superdir); if (err) { @@ -1995,7 +2127,7 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { .d.type = LFS_TYPE_SUPERBLOCK, .d.elen = sizeof(superblock.d) - sizeof(superblock.d.magic) - 4, .d.nlen = sizeof(superblock.d.magic), - .d.version = 0x00010001, + .d.version = LFS_DISK_VERSION, .d.magic = {"littlefs"}, .d.block_size = lfs->cfg->block_size, .d.block_count = lfs->cfg->block_count, @@ -2006,9 +2138,10 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { superdir.d.size = sizeof(superdir.d) + sizeof(superblock.d) + 4; // write both pairs to be safe + lfs_superblock_tole32(&superblock.d); bool valid = false; for (int i = 0; i < 2; i++) { - int err = lfs_dir_commit(lfs, &superdir, (struct lfs_region[]){ + err = lfs_dir_commit(lfs, &superdir, (struct lfs_region[]){ {sizeof(superdir.d), sizeof(superblock.d), &superblock.d, sizeof(superblock.d)} }, 1); @@ -2040,9 +2173,10 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { } // setup free lookahead - lfs->free.begin = -lfs->cfg->lookahead; - lfs->free.off = lfs->cfg->lookahead; - lfs->free.end = lfs->free.begin + lfs->free.off + lfs->cfg->block_count; + lfs->free.begin = 0; + lfs->free.size = 0; + lfs->free.off = 0; + lfs_alloc_ack(lfs); // load superblock lfs_dir_t dir; @@ -2053,8 +2187,9 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { } if (!err) { - int err = lfs_bd_read(lfs, dir.pair[0], sizeof(dir.d), + err = lfs_bd_read(lfs, dir.pair[0], sizeof(dir.d), &superblock.d, sizeof(superblock.d)); + lfs_superblock_fromle32(&superblock.d); if (err) { return err; } @@ -2068,10 +2203,11 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { return LFS_ERR_CORRUPT; } - if (superblock.d.version > (0x00010001 | 0x0000ffff)) { - LFS_ERROR("Invalid version %d.%d", - 0xffff & (superblock.d.version >> 16), - 0xffff & (superblock.d.version >> 0)); + uint16_t major_version = (0xffff & (superblock.d.version >> 16)); + uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); + if ((major_version != LFS_DISK_VERSION_MAJOR || + minor_version > LFS_DISK_VERSION_MINOR)) { + LFS_ERROR("Invalid version %d.%d", major_version, minor_version); return LFS_ERR_INVAL; } @@ -2109,15 +2245,16 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) { // iterate over contents while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size)-4) { - int err = lfs_bd_read(lfs, dir.pair[0], dir.off, + err = lfs_bd_read(lfs, dir.pair[0], dir.off, &entry.d, sizeof(entry.d)); + lfs_entry_fromle32(&entry.d); if (err) { return err; } dir.off += lfs_entry_size(&entry); if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) { - int err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL, + err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL, entry.d.u.file.head, entry.d.u.file.size, cb, data); if (err) { return err; @@ -2151,7 +2288,7 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) { } } } - + return 0; } @@ -2171,7 +2308,7 @@ static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir) { return true; } - int err = lfs_dir_fetch(lfs, pdir, pdir->d.tail); + err = lfs_dir_fetch(lfs, pdir, pdir->d.tail); if (err) { return err; } @@ -2185,7 +2322,7 @@ static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], if (lfs_pairisnull(lfs->root)) { return 0; } - + parent->d.tail[0] = 0; parent->d.tail[1] = 1; @@ -2197,7 +2334,7 @@ static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], } while (true) { - int err = lfs_dir_next(lfs, parent, entry); + err = lfs_dir_next(lfs, parent, entry); if (err && err != LFS_ERR_NOENT) { return err; } @@ -2231,13 +2368,13 @@ static int lfs_moved(lfs_t *lfs, const void *e) { // iterate over all directory directory entries lfs_entry_t entry; while (!lfs_pairisnull(cwd.d.tail)) { - int err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); + err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); if (err) { return err; } while (true) { - int err = lfs_dir_next(lfs, &cwd, &entry); + err = lfs_dir_next(lfs, &cwd, &entry); if (err && err != LFS_ERR_NOENT) { return err; } @@ -2246,7 +2383,7 @@ static int lfs_moved(lfs_t *lfs, const void *e) { break; } - if (!(0x80 & entry.d.type) && + if (!(0x80 & entry.d.type) && memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { return true; } @@ -2368,7 +2505,7 @@ int lfs_deorphan(lfs_t *lfs) { // check entries for moves lfs_entry_t entry; while (true) { - int err = lfs_dir_next(lfs, &cwd, &entry); + err = lfs_dir_next(lfs, &cwd, &entry); if (err && err != LFS_ERR_NOENT) { return err; } @@ -2387,7 +2524,7 @@ int lfs_deorphan(lfs_t *lfs) { if (moved) { LFS_DEBUG("Found move %d %d", entry.d.u.dir[0], entry.d.u.dir[1]); - int err = lfs_dir_remove(lfs, &cwd, &entry); + err = lfs_dir_remove(lfs, &cwd, &entry); if (err) { return err; } @@ -2395,7 +2532,7 @@ int lfs_deorphan(lfs_t *lfs) { LFS_DEBUG("Found partial move %d %d", entry.d.u.dir[0], entry.d.u.dir[1]); entry.d.type &= ~0x80; - int err = lfs_dir_update(lfs, &cwd, &entry, NULL); + err = lfs_dir_update(lfs, &cwd, &entry, NULL); if (err) { return err; } diff --git a/lfs.h b/lfs.h index 51d0a862302..ad379852b71 100644 --- a/lfs.h +++ b/lfs.h @@ -22,6 +22,23 @@ #include +/// Version info /// + +// Software library version +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS_VERSION 0x00010003 +#define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16)) +#define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >> 0)) + +// Version of On-disk data structures +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS_DISK_VERSION 0x00010001 +#define LFS_DISK_VERSION_MAJOR (0xffff & (LFS_DISK_VERSION >> 16)) +#define LFS_DISK_VERSION_MINOR (0xffff & (LFS_DISK_VERSION >> 0)) + + /// Definitions /// // Type definitions @@ -41,16 +58,18 @@ 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_EXIST = -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_BADF = -9, // Bad file number + 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 @@ -99,14 +118,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 @@ -121,11 +140,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. @@ -239,8 +260,9 @@ typedef struct lfs_superblock { typedef struct lfs_free { lfs_block_t begin; - lfs_block_t end; + lfs_block_t size; lfs_block_t off; + lfs_block_t ack; uint32_t *buffer; } lfs_free_t; @@ -361,6 +383,11 @@ lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence); +// Truncates the size of the file to the specified size +// +// Returns a negative error code on failure. +int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size); + // Return the position of the file // // Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR) diff --git a/lfs_util.h b/lfs_util.h index 9d23ab4eb00..0b7ccaef472 100644 --- a/lfs_util.h +++ b/lfs_util.h @@ -18,13 +18,60 @@ #ifndef LFS_UTIL_H #define LFS_UTIL_H -#include #include +#include +#include + +#ifndef LFS_NO_MALLOC +#include +#endif +#ifndef LFS_NO_ASSERT +#include +#endif +#if !defined(LFS_NO_DEBUG) || !defined(LFS_NO_WARN) || !defined(LFS_NO_ERROR) #include +#endif + +// Macros, may be replaced by system specific wrappers. Arguments to these +// macros must not have side-effects as the macros can be removed for a smaller +// code footprint -// Builtin functions, these may be replaced by more -// efficient implementations in the system +// Logging functions +#ifndef LFS_NO_DEBUG +#define LFS_DEBUG(fmt, ...) \ + printf("lfs debug:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_DEBUG(fmt, ...) +#endif + +#ifndef LFS_NO_WARN +#define LFS_WARN(fmt, ...) \ + printf("lfs warn:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_WARN(fmt, ...) +#endif + +#ifndef LFS_NO_ERROR +#define LFS_ERROR(fmt, ...) \ + printf("lfs error:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_ERROR(fmt, ...) +#endif + +// Runtime assertions +#ifndef LFS_NO_ASSERT +#define LFS_ASSERT(test) assert(test) +#else +#define LFS_ASSERT(test) +#endif + + +// Builtin functions, these may be replaced by more efficient +// toolchain-specific implementations. LFS_NO_INTRINSICS falls back to a more +// expensive basic C implementation for debugging purposes + +// Min/max functions for unsigned 32-bit numbers static inline uint32_t lfs_max(uint32_t a, uint32_t b) { return (a > b) ? a : b; } @@ -33,31 +80,92 @@ static inline uint32_t lfs_min(uint32_t a, uint32_t b) { return (a < b) ? a : b; } -static inline uint32_t lfs_ctz(uint32_t a) { - return __builtin_ctz(a); -} - +// Find the next smallest power of 2 less than or equal to a static inline uint32_t lfs_npw2(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) return 32 - __builtin_clz(a-1); +#else + uint32_t r = 0; + uint32_t s; + a -= 1; + s = (a > 0xffff) << 4; a >>= s; r |= s; + s = (a > 0xff ) << 3; a >>= s; r |= s; + s = (a > 0xf ) << 2; a >>= s; r |= s; + s = (a > 0x3 ) << 1; a >>= s; r |= s; + return (r | (a >> 1)) + 1; +#endif } +// Count the number of trailing binary zeros in a +// lfs_ctz(0) may be undefined +static inline uint32_t lfs_ctz(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) + return __builtin_ctz(a); +#else + return lfs_npw2((a & -a) + 1) - 1; +#endif +} + +// Count the number of binary ones in a static inline uint32_t lfs_popc(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) return __builtin_popcount(a); +#else + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; +#endif } +// Find the sequence comparison of a and b, this is the distance +// between a and b ignoring overflow static inline int lfs_scmp(uint32_t a, uint32_t b) { return (int)(unsigned)(a - b); } -// CRC-32 with polynomial = 0x04c11db7 +// Convert from 32-bit little-endian to native order +static inline uint32_t lfs_fromle32(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && ( \ + (defined( BYTE_ORDER ) && BYTE_ORDER == ORDER_LITTLE_ENDIAN ) || \ + (defined(__BYTE_ORDER ) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN ) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) + return a; +#elif !defined(LFS_NO_INTRINSICS) && ( \ + (defined( BYTE_ORDER ) && BYTE_ORDER == ORDER_BIG_ENDIAN ) || \ + (defined(__BYTE_ORDER ) && __BYTE_ORDER == __ORDER_BIG_ENDIAN ) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) + return __builtin_bswap32(a); +#else + return (((uint8_t*)&a)[0] << 0) | + (((uint8_t*)&a)[1] << 8) | + (((uint8_t*)&a)[2] << 16) | + (((uint8_t*)&a)[3] << 24); +#endif +} + +// Convert to 32-bit little-endian from native order +static inline uint32_t lfs_tole32(uint32_t a) { + return lfs_fromle32(a); +} + +// Calculate CRC-32 with polynomial = 0x04c11db7 void lfs_crc(uint32_t *crc, const void *buffer, size_t size); +// Allocate memory, only used if buffers are not provided to littlefs +static inline void *lfs_malloc(size_t size) { +#ifndef LFS_NO_MALLOC + return malloc(size); +#else + return NULL; +#endif +} -// Logging functions, these may be replaced by system-specific -// logging functions -#define LFS_DEBUG(fmt, ...) printf("lfs debug: " fmt "\n", __VA_ARGS__) -#define LFS_WARN(fmt, ...) printf("lfs warn: " fmt "\n", __VA_ARGS__) -#define LFS_ERROR(fmt, ...) printf("lfs error: " fmt "\n", __VA_ARGS__) +// Deallocate memory, only used if buffers are not provided to littlefs +static inline void lfs_free(void *p) { +#ifndef LFS_NO_MALLOC + free(p); +#endif +} #endif diff --git a/tests/template.fmt b/tests/template.fmt index 85f00bdd33a..a53f0c7e24e 100644 --- a/tests/template.fmt +++ b/tests/template.fmt @@ -7,11 +7,11 @@ // test stuff -void test_log(const char *s, uintmax_t v) {{ +static void test_log(const char *s, uintmax_t v) {{ printf("%s: %jd\n", s, v); }} -void test_assert(const char *file, unsigned line, +static void test_assert(const char *file, unsigned line, const char *s, uintmax_t v, uintmax_t e) {{ static const char *last[6] = {{0, 0}}; if (v != e || !(last[0] == s || last[1] == s || @@ -37,7 +37,8 @@ void test_assert(const char *file, unsigned line, // utility functions for traversals -int test_count(void *p, lfs_block_t b) {{ +static int __attribute__((used)) test_count(void *p, lfs_block_t b) {{ + (void)b; unsigned *u = (unsigned*)p; *u += 1; return 0; @@ -58,7 +59,7 @@ lfs_size_t size; lfs_size_t wsize; lfs_size_t rsize; -uintmax_t res; +uintmax_t test; #ifndef LFS_READ_SIZE #define LFS_READ_SIZE 16 @@ -96,7 +97,7 @@ const struct lfs_config cfg = {{ // Entry point -int main() {{ +int main(void) {{ lfs_emubd_create(&cfg, "blocks"); {tests} diff --git a/tests/test.py b/tests/test.py index aa125c7c2fd..24b0d1a37b6 100755 --- a/tests/test.py +++ b/tests/test.py @@ -14,22 +14,34 @@ def generate(test): match = re.match('(?: *\n)*( *)(.*)=>(.*);', line, re.DOTALL | re.MULTILINE) if match: tab, test, expect = match.groups() - lines.append(tab+'res = {test};'.format(test=test.strip())) - lines.append(tab+'test_assert("{name}", res, {expect});'.format( + lines.append(tab+'test = {test};'.format(test=test.strip())) + lines.append(tab+'test_assert("{name}", test, {expect});'.format( name = re.match('\w*', test.strip()).group(), expect = expect.strip())) else: lines.append(line) + # Create test file with open('test.c', 'w') as file: file.write(template.format(tests='\n'.join(lines))) + # Remove build artifacts to force rebuild + try: + os.remove('test.o') + os.remove('lfs') + except OSError: + pass + def compile(): - os.environ['CFLAGS'] = os.environ.get('CFLAGS', '') + ' -Werror' - subprocess.check_call(['make', '--no-print-directory', '-s'], env=os.environ) + subprocess.check_call([ + os.environ.get('MAKE', 'make'), + '--no-print-directory', '-s']) def execute(): - subprocess.check_call(["./lfs"]) + if 'EXEC' in os.environ: + subprocess.check_call([os.environ['EXEC'], "./lfs"]) + else: + subprocess.check_call(["./lfs"]) def main(test=None): if test and not test.startswith('-'): diff --git a/tests/test_alloc.sh b/tests/test_alloc.sh index aaae6551e16..493270d103e 100755 --- a/tests/test_alloc.sh +++ b/tests/test_alloc.sh @@ -266,6 +266,40 @@ tests/test.py << TEST lfs_mkdir(&lfs, "exhaustiondir2") => LFS_ERR_NOSPC; TEST +echo "--- Split dir test ---" +rm -rf blocks +tests/test.py << TEST + lfs_format(&lfs, &cfg) => 0; +TEST +tests/test.py << TEST + lfs_mount(&lfs, &cfg) => 0; + + // create one block whole for half a directory + lfs_file_open(&lfs, &file[0], "bump", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfs_file_write(&lfs, &file[0], (void*)"hi", 2) => 2; + lfs_file_close(&lfs, &file[0]) => 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 < (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; + + // open whole + lfs_remove(&lfs, "bump") => 0; + + lfs_mkdir(&lfs, "splitdir") => 0; + lfs_file_open(&lfs, &file[0], "splitdir/bump", + LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfs_file_write(&lfs, &file[0], buffer, size) => LFS_ERR_NOSPC; + lfs_file_close(&lfs, &file[0]) => 0; + + lfs_unmount(&lfs) => 0; +TEST echo "--- Results ---" tests/stats.py diff --git a/tests/test_dirs.sh b/tests/test_dirs.sh index 431889007a6..9f9733dd712 100755 --- a/tests/test_dirs.sh +++ b/tests/test_dirs.sh @@ -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; @@ -220,7 +220,7 @@ tests/test.py << TEST lfs_mount(&lfs, &cfg) => 0; lfs_mkdir(&lfs, "warmpotato") => 0; lfs_mkdir(&lfs, "warmpotato/mushy") => 0; - lfs_rename(&lfs, "hotpotato", "warmpotato") => LFS_ERR_INVAL; + lfs_rename(&lfs, "hotpotato", "warmpotato") => LFS_ERR_NOTEMPTY; lfs_remove(&lfs, "warmpotato/mushy") => 0; lfs_rename(&lfs, "hotpotato", "warmpotato") => 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 @@ -285,7 +285,7 @@ TEST echo "--- Recursive remove ---" tests/test.py << TEST lfs_mount(&lfs, &cfg) => 0; - lfs_remove(&lfs, "coldpotato") => LFS_ERR_INVAL; + lfs_remove(&lfs, "coldpotato") => LFS_ERR_NOTEMPTY; lfs_dir_open(&lfs, &dir[0], "coldpotato") => 0; lfs_dir_read(&lfs, &dir[0], &info) => 1; @@ -328,7 +328,7 @@ 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); diff --git a/tests/test_files.sh b/tests/test_files.sh index 6086107c89b..444346371b5 100755 --- a/tests/test_files.sh +++ b/tests/test_files.sh @@ -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; diff --git a/tests/test_seek.sh b/tests/test_seek.sh index 6600cb2f5c5..3b46892b6ea 100755 --- a/tests/test_seek.sh +++ b/tests/test_seek.sh @@ -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; diff --git a/tests/test_truncate.sh b/tests/test_truncate.sh new file mode 100755 index 00000000000..da5ccaf033f --- /dev/null +++ b/tests/test_truncate.sh @@ -0,0 +1,158 @@ +#!/bin/bash +set -eu + +SMALLSIZE=32 +MEDIUMSIZE=2048 +LARGESIZE=8192 + +echo "=== Truncate tests ===" +rm -rf blocks +tests/test.py << TEST + lfs_format(&lfs, &cfg) => 0; +TEST + +truncate_test() { +STARTSIZES="$1" +STARTSEEKS="$2" +HOTSIZES="$3" +COLDSIZES="$4" +tests/test.py << TEST + static const lfs_off_t startsizes[] = {$STARTSIZES}; + static const lfs_off_t startseeks[] = {$STARTSEEKS}; + static const lfs_off_t hotsizes[] = {$HOTSIZES}; + + lfs_mount(&lfs, &cfg) => 0; + + for (int i = 0; i < sizeof(startsizes)/sizeof(startsizes[0]); i++) { + sprintf((char*)buffer, "hairyhead%d", i); + lfs_file_open(&lfs, &file[0], (const char*)buffer, + LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0; + + strcpy((char*)buffer, "hair"); + size = strlen((char*)buffer); + for (int j = 0; j < startsizes[i]; j += size) { + lfs_file_write(&lfs, &file[0], buffer, size) => size; + } + lfs_file_size(&lfs, &file[0]) => startsizes[i]; + + if (startseeks[i] != startsizes[i]) { + lfs_file_seek(&lfs, &file[0], + startseeks[i], LFS_SEEK_SET) => startseeks[i]; + } + + lfs_file_truncate(&lfs, &file[0], hotsizes[i]) => 0; + lfs_file_size(&lfs, &file[0]) => hotsizes[i]; + + lfs_file_close(&lfs, &file[0]) => 0; + } + + lfs_unmount(&lfs) => 0; +TEST +tests/test.py << TEST + static const lfs_off_t startsizes[] = {$STARTSIZES}; + static const lfs_off_t hotsizes[] = {$HOTSIZES}; + static const lfs_off_t coldsizes[] = {$COLDSIZES}; + + lfs_mount(&lfs, &cfg) => 0; + + for (int i = 0; i < sizeof(startsizes)/sizeof(startsizes[0]); i++) { + sprintf((char*)buffer, "hairyhead%d", i); + lfs_file_open(&lfs, &file[0], (const char*)buffer, LFS_O_RDWR) => 0; + lfs_file_size(&lfs, &file[0]) => hotsizes[i]; + + size = strlen("hair"); + int j = 0; + for (; j < startsizes[i] && j < hotsizes[i]; j += size) { + lfs_file_read(&lfs, &file[0], buffer, size) => size; + memcmp(buffer, "hair", size) => 0; + } + + for (; j < hotsizes[i]; j += size) { + lfs_file_read(&lfs, &file[0], buffer, size) => size; + memcmp(buffer, "\0\0\0\0", size) => 0; + } + + lfs_file_truncate(&lfs, &file[0], coldsizes[i]) => 0; + lfs_file_size(&lfs, &file[0]) => coldsizes[i]; + + lfs_file_close(&lfs, &file[0]) => 0; + } + + lfs_unmount(&lfs) => 0; +TEST +tests/test.py << TEST + static const lfs_off_t startsizes[] = {$STARTSIZES}; + static const lfs_off_t hotsizes[] = {$HOTSIZES}; + static const lfs_off_t coldsizes[] = {$COLDSIZES}; + + lfs_mount(&lfs, &cfg) => 0; + + for (int i = 0; i < sizeof(startsizes)/sizeof(startsizes[0]); i++) { + sprintf((char*)buffer, "hairyhead%d", i); + lfs_file_open(&lfs, &file[0], (const char*)buffer, LFS_O_RDONLY) => 0; + lfs_file_size(&lfs, &file[0]) => coldsizes[i]; + + size = strlen("hair"); + int j = 0; + for (; j < startsizes[i] && j < hotsizes[i] && j < coldsizes[i]; + j += size) { + lfs_file_read(&lfs, &file[0], buffer, size) => size; + memcmp(buffer, "hair", size) => 0; + } + + for (; j < coldsizes[i]; j += size) { + lfs_file_read(&lfs, &file[0], buffer, size) => size; + memcmp(buffer, "\0\0\0\0", size) => 0; + } + + lfs_file_close(&lfs, &file[0]) => 0; + } + + lfs_unmount(&lfs) => 0; +TEST +} + +echo "--- Cold shrinking truncate ---" +truncate_test \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" + +echo "--- Cold expanding truncate ---" +truncate_test \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" + +echo "--- Warm shrinking truncate ---" +truncate_test \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" \ + " 0, 0, 0, 0, 0" + +echo "--- Warm expanding truncate ---" +truncate_test \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" + +echo "--- Mid-file shrinking truncate ---" +truncate_test \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" \ + " $LARGESIZE, $LARGESIZE, $LARGESIZE, $LARGESIZE, $LARGESIZE" \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" \ + " 0, 0, 0, 0, 0" + +echo "--- Mid-file expanding truncate ---" +truncate_test \ + " 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE, 2*$LARGESIZE" \ + " 0, 0, $SMALLSIZE, $MEDIUMSIZE, $LARGESIZE" \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" \ + "2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE, 2*$LARGESIZE" + +echo "--- Results ---" +tests/stats.py