Browse Source

Merge branch 'master' of https://github.com/geky/littlefs

xieyangrun 6 years ago
parent
commit
18d9f1ac40
28 changed files with 4039 additions and 1651 deletions
  1. 131 59
      .travis.yml
  2. 729 380
      DESIGN.md
  3. 5 2
      Makefile
  4. 115 59
      README.md
  5. 735 318
      SPEC.md
  6. 86 9
      emubd/lfs_emubd.c
  7. 4 0
      emubd/lfs_emubd.h
  8. 814 536
      lfs.c
  9. 283 138
      lfs.h
  10. 5 3
      lfs_util.c
  11. 39 5
      lfs_util.h
  12. 61 0
      scripts/prefix.py
  13. 44 0
      tests/corrupt.py
  14. 112 0
      tests/debug.py
  15. 2 2
      tests/stats.py
  16. 18 8
      tests/template.fmt
  17. 1 1
      tests/test.py
  18. 98 41
      tests/test_alloc.sh
  19. 286 0
      tests/test_attrs.sh
  20. 7 6
      tests/test_corrupt.sh
  21. 23 23
      tests/test_dirs.sh
  22. 221 0
      tests/test_entries.sh
  23. 7 7
      tests/test_files.sh
  24. 15 14
      tests/test_format.sh
  25. 110 14
      tests/test_move.sh
  26. 15 11
      tests/test_orphan.sh
  27. 58 0
      tests/test_paths.sh
  28. 15 15
      tests/test_seek.sh

+ 131 - 59
.travis.yml

@@ -18,10 +18,11 @@ script:
   - 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 test QUIET=1 CFLAGS+="-DLFS_READ_SIZE=1      -DLFS_CACHE_SIZE=4"
+  - make test QUIET=1 CFLAGS+="-DLFS_READ_SIZE=512    -DLFS_CACHE_SIZE=512 -DLFS_BLOCK_CYCLES=16"
+  - make test QUIET=1 CFLAGS+="-DLFS_BLOCK_COUNT=1023 -DLFS_LOOKAHEAD_SIZE=256"
 
+  - make clean test QUIET=1 CFLAGS+="-DLFS_INLINE_MAX=0"
   - make clean test QUIET=1 CFLAGS+="-DLFS_NO_INTRINSICS"
 
   # compile and find the code size with the smallest configuration
@@ -35,7 +36,7 @@ script:
     if [ "$TRAVIS_TEST_RESULT" -eq 0 ]
     then
         CURR=$(tail -n1 sizes | awk '{print $1}')
-        PREV=$(curl -u $GEKY_BOT_STATUSES https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/master \
+        PREV=$(curl -u "$GEKY_BOT_STATUSES" https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/master \
             | jq -re "select(.sha != \"$TRAVIS_COMMIT\")
                 | .statuses[] | select(.context == \"$STAGE/$NAME\").description
                 | capture(\"code size is (?<size>[0-9]+)\").size" \
@@ -100,9 +101,10 @@ jobs:
       env:
         - STAGE=test
         - NAME=littlefs-fuse
+      if: branch !~ -prefix$
       install:
         - sudo apt-get install libfuse-dev
-        - git clone --depth 1 https://github.com/geky/littlefs-fuse
+        - git clone --depth 1 https://github.com/geky/littlefs-fuse -b v2-alpha
         - fusermount -V
         - gcc --version
       before_script:
@@ -112,7 +114,7 @@ jobs:
 
         - mkdir mount
         - sudo chmod a+rw /dev/loop0
-        - dd if=/dev/zero bs=512 count=2048 of=disk
+        - dd if=/dev/zero bs=512 count=4096 of=disk
         - losetup /dev/loop0 disk
       script:
         # self-host test
@@ -125,73 +127,143 @@ jobs:
         - mkdir mount/littlefs
         - cp -r $(git ls-tree --name-only HEAD) mount/littlefs
         - cd mount/littlefs
-        - ls
+        - stat .
+        - ls -flh
         - make -B test_dirs test_files QUIET=1
 
-      # Automatically update releases
+    # self-host with littlefs-fuse for fuzz test
+    - stage: test
+      env:
+        - STAGE=test
+        - NAME=littlefs-migration
+      install:
+        - sudo apt-get install libfuse-dev
+        - git clone --depth 1 https://github.com/geky/littlefs-fuse -b v2-alpha v2
+        - git clone --depth 1 https://github.com/geky/littlefs-fuse -b v1 v1
+        - fusermount -V
+        - gcc --version
+      before_script:
+        # setup disk for littlefs-fuse
+        - rm -rf v2/littlefs/*
+        - cp -r $(git ls-tree --name-only HEAD) v2/littlefs
+
+        - mkdir mount
+        - sudo chmod a+rw /dev/loop0
+        - dd if=/dev/zero bs=512 count=4096 of=disk
+        - losetup /dev/loop0 disk
+      script:
+        # compile v1 and v2
+        - make -C v1
+        - make -C v2
+
+        # run self-host test with v1
+        - v1/lfs --format /dev/loop0
+        - v1/lfs /dev/loop0 mount
+
+        - ls mount
+        - mkdir mount/littlefs
+        - cp -r $(git ls-tree --name-only HEAD) mount/littlefs
+        - cd mount/littlefs
+        - stat .
+        - ls -flh
+        - make -B test_dirs test_files QUIET=1
+
+        # attempt to migrate
+        - cd ../..
+        - fusermount -u mount
+
+        - v2/lfs --migrate /dev/loop0
+        - v2/lfs /dev/loop0 mount
+
+        # run self-host test with v2 right where we left off
+        - ls mount
+        - cd mount/littlefs
+        - stat .
+        - ls -flh
+        - make -B test_dirs test_files QUIET=1
+
+    # Automatically create releases
     - stage: deploy
       env:
         - STAGE=deploy
         - NAME=deploy
       script:
-        # Find 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)))
-        # Grab latests patch from repo tags, default to 0, needs finagling to get past github's pagination api
-        - PREV_URL=https://api.github.com/repos/$TRAVIS_REPO_SLUG/git/refs/tags/v$LFS_VERSION_MAJOR.$LFS_VERSION_MINOR.
-        - PREV_URL=$(curl -u "$GEKY_BOT_RELEASES" "$PREV_URL" -I
-                | sed -n '/^Link/{s/.*<\(.*\)>; rel="last"/\1/;p;q0};$q1'
-                || echo $PREV_URL)
-        - LFS_VERSION_PATCH=$(curl -u "$GEKY_BOT_RELEASES" "$PREV_URL"
-                | jq 'map(.ref | match("\\bv.*\\..*\\.(.*)$";"g")
-                    .captures[].string | tonumber) | max + 1'
-                || echo 0)
-        # We have our new version
-        - LFS_VERSION="v$LFS_VERSION_MAJOR.$LFS_VERSION_MINOR.$LFS_VERSION_PATCH"
-        - echo "VERSION $LFS_VERSION"
         - |
+          bash << 'SCRIPT'
+          set -ev
+          # Find 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)))
+          # Grab latests patch from repo tags, default to 0, needs finagling
+          # to get past github's pagination api
+          PREV_URL=https://api.github.com/repos/$TRAVIS_REPO_SLUG/git/refs/tags/v$LFS_VERSION_MAJOR.$LFS_VERSION_MINOR.
+          PREV_URL=$(curl -u "$GEKY_BOT_RELEASES" "$PREV_URL" -I \
+              | sed -n '/^Link/{s/.*<\(.*\)>; rel="last"/\1/;p;q0};$q1' \
+              || echo $PREV_URL)
+          LFS_VERSION_PATCH=$(curl -u "$GEKY_BOT_RELEASES" "$PREV_URL" \
+              | jq 'map(.ref | match("\\bv.*\\..*\\.(.*)$";"g")
+                  .captures[].string | tonumber) | max + 1' \
+              || echo 0)
+          # We have our new version
+          LFS_VERSION="v$LFS_VERSION_MAJOR.$LFS_VERSION_MINOR.$LFS_VERSION_PATCH"
+          echo "VERSION $LFS_VERSION"
           # Check that we're the most recent commit
           CURRENT_COMMIT=$(curl -f -u "$GEKY_BOT_RELEASES" \
-                https://api.github.com/repos/$TRAVIS_REPO_SLUG/commits/master \
-                | jq -re '.sha')
-          if [ "$TRAVIS_COMMIT" == "$CURRENT_COMMIT" ]
+              https://api.github.com/repos/$TRAVIS_REPO_SLUG/commits/master \
+              | jq -re '.sha')
+          [ "$TRAVIS_COMMIT" == "$CURRENT_COMMIT" ] || exit 0
+          # Create major branch
+          git branch v$LFS_VERSION_MAJOR HEAD
+          # Create major prefix branch
+          git config user.name "geky bot"
+          git config user.email "bot@geky.net"
+          git fetch https://github.com/$TRAVIS_REPO_SLUG.git \
+              --depth=50 v$LFS_VERSION_MAJOR-prefix || true
+          ./scripts/prefix.py lfs$LFS_VERSION_MAJOR
+          git branch v$LFS_VERSION_MAJOR-prefix $( \
+              git commit-tree $(git write-tree) \
+                  $(git rev-parse --verify -q FETCH_HEAD | sed -e 's/^/-p /') \
+                  -p HEAD \
+                  -m "Generated v$LFS_VERSION_MAJOR prefixes")
+          git reset --hard
+          # Update major version branches (vN and vN-prefix)
+          git push https://$GEKY_BOT_RELEASES@github.com/$TRAVIS_REPO_SLUG.git \
+              v$LFS_VERSION_MAJOR \
+              v$LFS_VERSION_MAJOR-prefix
+          # Create patch version tag (vN.N.N)
+          curl -f -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\"
+              }"
+          # Create minor release?
+          [[ "$LFS_VERSION" == *.0 ]] || exit 0
+          # Build release notes
+          PREV=$(git tag --sort=-v:refname -l "v*.0" | head -1)
+          if [ ! -z "$PREV" ]
           then
-            # Create a simple tag
-            curl -f -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\"
-                }"
-            # Minor release?
-            if [[ "$LFS_VERSION" == *.0 ]]
-            then
-              # Build release notes
-              PREV=$(git tag --sort=-v:refname -l "v*.0" | head -1)
-              if [ ! -z "$PREV" ]
-              then
-                  echo "PREV $PREV"
-                  CHANGES=$'### Changes\n\n'$( \
-                      git log --oneline $PREV.. --grep='^Merge' --invert-grep)
-                  printf "CHANGES\n%s\n\n" "$CHANGES"
-              fi
-              # Create the release
-              curl -f -u "$GEKY_BOT_RELEASES" -X POST \
-                  https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases \
-                  -d "{
-                      \"tag_name\": \"$LFS_VERSION\",
-                      \"name\": \"${LFS_VERSION%.0}\",
-                      \"draft\": true,
-                      \"body\": $(jq -sR '.' <<< "$CHANGES")
-                  }"
-            fi
+              echo "PREV $PREV"
+              CHANGES=$'### Changes\n\n'$( \
+                  git log --oneline $PREV.. --grep='^Merge' --invert-grep)
+              printf "CHANGES\n%s\n\n" "$CHANGES"
           fi
+          # Create the release
+          curl -f -u "$GEKY_BOT_RELEASES" -X POST \
+              https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases \
+              -d "{
+                  \"tag_name\": \"$LFS_VERSION\",
+                  \"name\": \"${LFS_VERSION%.0}\",
+                  \"draft\": true,
+                  \"body\": $(jq -sR '.' <<< "$CHANGES")
+              }" #"
+          SCRIPT
 
 # Manage statuses
 before_install:
   - |
-    curl -u $GEKY_BOT_STATUSES -X POST \
+    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\",
@@ -202,7 +274,7 @@ before_install:
 
 after_failure:
   - |
-    curl -u $GEKY_BOT_STATUSES -X POST \
+    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\",
@@ -213,7 +285,7 @@ after_failure:
 
 after_success:
   - |
-    curl -u $GEKY_BOT_STATUSES -X POST \
+    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\",

File diff suppressed because it is too large
+ 729 - 380
DESIGN.md


+ 5 - 2
Makefile

@@ -26,7 +26,9 @@ override CFLAGS += -m$(WORD)
 endif
 override CFLAGS += -I.
 override CFLAGS += -std=c99 -Wall -pedantic
-override CFLAGS += -Wshadow -Wunused-parameter -Wjump-misses-init -Wsign-compare
+override CFLAGS += -Wextra -Wshadow -Wjump-misses-init
+# Remove missing-field-initializers because of GCC bug
+override CFLAGS += -Wno-missing-field-initializers
 
 
 all: $(TARGET)
@@ -38,7 +40,8 @@ size: $(OBJ)
 
 .SUFFIXES:
 test: test_format test_dirs test_files test_seek test_truncate \
-	test_interspersed test_alloc test_paths test_orphan test_move test_corrupt
+	test_entries test_interspersed test_alloc test_paths test_attrs \
+	test_move test_orphan test_corrupt
 	@rm test.c
 test_%: tests/test_%.sh
 

+ 115 - 59
README.md

@@ -1,6 +1,6 @@
-## The little filesystem
+## littlefs
 
-A little fail-safe filesystem designed for embedded systems.
+A little fail-safe filesystem designed for microcontrollers.
 
 ```
    | | |     .---._____
@@ -11,17 +11,19 @@ A little fail-safe filesystem designed for embedded systems.
    | | |
 ```
 
-**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.
+**Power-loss resilience** - littlefs is designed to handle random power
+failures. All file operations have strong copy-on-write guarantees and if
+power is lost the filesystem will fall back to the last known good state.
 
-**Power-loss resilient** - The littlefs is designed for systems that may have
-random power failures. The littlefs has strong copy-on-write guarantees and
-storage on disk is always kept in a valid state.
+**Dynamic wear leveling** - littlefs is designed with flash in mind, and
+provides wear leveling over dynamic blocks. Additionally, littlefs can
+detect bad blocks and work around them.
 
-**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.
+**Bounded RAM/ROM** - littlefs is designed to work with a small amount of
+memory. RAM usage is strictly bounded, which means RAM consumption does not
+change as the filesystem grows. The filesystem contains no unbounded
+recursion and dynamic memory is limited to configurable buffers that can be
+provided statically.
 
 ## Example
 
@@ -49,7 +51,8 @@ const struct lfs_config cfg = {
     .prog_size = 16,
     .block_size = 4096,
     .block_count = 128,
-    .lookahead = 128,
+    .cache_size = 16,
+    .lookahead_size = 16,
 };
 
 // entry point
@@ -90,11 +93,11 @@ int main(void) {
 Detailed documentation (or at least as much detail as is currently available)
 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
-filesystem with the block device operations and dimensions, tweakable
-parameters that tradeoff memory usage for performance, and optional
-static buffers if the user wants to avoid dynamic memory.
+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 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
@@ -106,13 +109,13 @@ 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 committed to the
-filesystem until sync or close is called on the file.
+of power-loss. Additionally, no file updates are not actually committed to
+the filesystem until sync or close is called on the file.
 
 ## Other notes
 
-All littlefs calls have the potential to return a negative error code. The 
-errors can be either one of those found in the `enum lfs_error` in 
+All littlefs calls 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.
 
 In the configuration struct, the `prog` and `erase` function provided by the
@@ -127,14 +130,60 @@ from memory, otherwise data integrity can not be guaranteed. If the `write`
 function does not perform caching, and therefore each `read` or `write` call
 hits the memory, the `sync` function can simply return 0.
 
-## Reference material
+## Design
 
-[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.
+At a high level, littlefs is a block based filesystem that uses small logs to
+store metadata and larger copy-on-write (COW) structures to store file data.
 
-[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.
+In littlefs, these ingredients form a sort of two-layered cake, with the small
+logs (called metadata pairs) providing fast updates to metadata anywhere on
+storage, while the COW structures store file data compactly and without any
+wear amplification cost.
+
+Both of these data structures are built out of blocks, which are fed by a
+common block allocator. By limiting the number of erases allowed on a block
+per allocation, the allocator provides dynamic wear leveling over the entire
+filesystem.
+
+```
+                    root
+                   .--------.--------.
+                   | A'| B'|         |
+                   |   |   |->       |
+                   |   |   |         |
+                   '--------'--------'
+                .----'   '--------------.
+       A       v                 B       v
+      .--------.--------.       .--------.--------.
+      | C'| D'|         |       | E'|new|         |
+      |   |   |->       |       |   | E'|->       |
+      |   |   |         |       |   |   |         |
+      '--------'--------'       '--------'--------'
+      .-'   '--.                  |   '------------------.
+     v          v              .-'                        v
+.--------.  .--------.        v                       .--------.
+|   C    |  |   D    |   .--------.       write       | new E  |
+|        |  |        |   |   E    |        ==>        |        |
+|        |  |        |   |        |                   |        |
+'--------'  '--------'   |        |                   '--------'
+                         '--------'                   .-'    |
+                         .-'    '-.    .-------------|------'
+                        v          v  v              v
+                   .--------.  .--------.       .--------.
+                   |   F    |  |   G    |       | new F  |
+                   |        |  |        |       |        |
+                   |        |  |        |       |        |
+                   '--------'  '--------'       '--------'
+```
+
+More details on how littlefs works can be found in [DESIGN.md](DESIGN.md) and
+[SPEC.md](SPEC.md).
+
+- [DESIGN.md](DESIGN.md) - A fully detailed dive into how littlefs works.
+  I would suggest reading it as the tradeoffs at work are quite interesting.
+
+- [SPEC.md](SPEC.md) - The on-disk specification of littlefs with all the
+  nitty-gritty details. May be useful for tooling development.
 
 ## Testing
 
@@ -148,9 +197,9 @@ make test
 
 ## License
 
-The littlefs is provided under the [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)
-license. See [LICENSE.md](LICENSE.md) for more information. Contributions to
-this project are accepted under the same license.
+The littlefs is provided under the [BSD-3-Clause] license. See
+[LICENSE.md](LICENSE.md) for more information. Contributions to this project
+are accepted under the same license.
 
 Individual files contain the following tag instead of the full license text.
 
@@ -161,32 +210,39 @@ License Identifiers that are here available: http://spdx.org/licenses/
 
 ## 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 on 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).
-
-[mklfs](https://github.com/whitecatboard/Lua-RTOS-ESP32/tree/master/components/mklfs/src) -
-A command line tool built by the [Lua RTOS](https://github.com/whitecatboard/Lua-RTOS-ESP32)
-guys for making littlefs images from a host PC. Supports Windows, Mac OS,
-and Linux.
-
-[SPIFFS](https://github.com/pellepl/spiffs) - Another excellent embedded
-filesystem for NOR flash. As a more traditional logging filesystem with full
-static wear-leveling, SPIFFS will likely outperform littlefs on small
-memories such as the internal flash on microcontrollers.
-
-[Dhara](https://github.com/dlbeer/dhara) - An interesting NAND flash
-translation layer designed for small MCUs. It offers static wear-leveling and
-power-resilience with only a fixed O(|address|) pointer structure stored on
-each block and in RAM.
+- [littlefs-fuse] - A [FUSE] 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.
+
+- [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][littlefs-js-demo].
+
+- [mklfs] - A command line tool built by the [Lua RTOS] guys for making
+  littlefs images from a host PC. Supports Windows, Mac OS, and Linux.
+
+- [Mbed OS] - The easiest way to get started with littlefs is to jump into Mbed
+  which already has block device drivers for most forms of embedded storage.
+  littlefs is available in Mbed OS as the [LittleFileSystem] class.
+
+- [SPIFFS] - Another excellent embedded filesystem for NOR flash. As a more
+  traditional logging filesystem with full static wear-leveling, SPIFFS will
+  likely outperform littlefs on small memories such as the internal flash on
+  microcontrollers.
+
+- [Dhara] - An interesting NAND flash translation layer designed for small
+  MCUs. It offers static wear-leveling and power-resilience with only a fixed
+  _O(|address|)_ pointer structure stored on each block and in RAM.
+
+
+[BSD-3-Clause]: https://spdx.org/licenses/BSD-3-Clause.html
+[littlefs-fuse]: https://github.com/geky/littlefs-fuse
+[FUSE]: https://github.com/libfuse/libfuse
+[littlefs-js]: https://github.com/geky/littlefs-js
+[littlefs-js-demo]:http://littlefs.geky.net/demo.html
+[mklfs]: https://github.com/whitecatboard/Lua-RTOS-ESP32/tree/master/components/mklfs/src
+[Lua RTOS]: https://github.com/whitecatboard/Lua-RTOS-ESP32
+[Mbed OS]: https://github.com/armmbed/mbed-os
+[LittleFileSystem]: https://os.mbed.com/docs/mbed-os/v5.12/apis/littlefilesystem.html
+[SPIFFS]: https://github.com/pellepl/spiffs
+[Dhara]: https://github.com/dlbeer/dhara

+ 735 - 318
SPEC.md

@@ -1,10 +1,10 @@
-## The little filesystem technical specification
+## littlefs technical specification
 
 This is the technical specification of the little filesystem. This document
 covers the technical details of how the littlefs is stored on disk for
-introspection and tooling development. This document assumes you are
-familiar with the design of the littlefs, for more info on how littlefs
-works check out [DESIGN.md](DESIGN.md).
+introspection and tooling. This document assumes you are familiar with the
+design of the littlefs, for more info on how littlefs works check
+out [DESIGN.md](DESIGN.md).
 
 ```
    | | |     .---._____
@@ -15,356 +15,773 @@ works check out [DESIGN.md](DESIGN.md).
    | | |
 ```
 
-## Some important details
+## Some quick notes
 
-- The littlefs is a block-based filesystem. This is, the disk is divided into
-  an array of evenly sized blocks that are used as the logical unit of storage
-  in littlefs. Block pointers are stored in 32 bits.
+- littlefs is a block-based filesystem. The disk is divided into an array of
+  evenly sized blocks that are used as the logical unit of storage.
 
-- There is no explicit free-list stored on disk, the littlefs only knows what
-  is in use in the filesystem.
+- Block pointers are stored in 32 bits, with the special value `0xffffffff`
+  representing a null block address.
 
-- The littlefs uses the value of 0xffffffff to represent a null block-pointer.
+- In addition to the logical block size (which usually matches the erase
+  block size), littlefs also uses a program block size and read block size.
+  These determine the alignment of block device operations, but don't need
+  to be consistent for portability.
 
-- All values in littlefs are stored in little-endian byte order.
+- By default, all values in littlefs are stored in little-endian byte order.
 
 ## Directories / Metadata pairs
 
-Metadata pairs form the backbone of the littlefs and provide a system for
-atomic updates. Even the superblock is stored in a metadata pair.
+Metadata pairs form the backbone of littlefs and provide a system for
+distributed atomic updates. Even the superblock is stored in a metadata pair.
 
 As their name suggests, a metadata pair is stored in two blocks, with one block
-acting as a redundant backup in case the other is corrupted. These two blocks
-could be anywhere in the disk and may not be next to each other, so any
-pointers to directory pairs need to be stored as two block pointers.
-
-Here's the layout of metadata blocks on disk:
-
-| offset | size          | description    |
-|--------|---------------|----------------|
-| 0x00   | 32 bits       | revision count |
-| 0x04   | 32 bits       | dir size       |
-| 0x08   | 64 bits       | tail pointer   |
-| 0x10   | size-16 bytes | dir entries    |
-| 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.
-Comparison between revision counts must use sequence comparison since the
-revision counts may overflow.
-
-**Dir size** - Size in bytes of the contents in the current metadata block,
-including the metadata-pair metadata. Additionally, the highest bit of the
-dir size may be set to indicate that the directory's contents continue on the
-next metadata-pair pointed to by the tail pointer.
-
-**Tail pointer** - Pointer to the next metadata-pair in the filesystem.
-A null pair-pointer (0xffffffff, 0xffffffff) indicates the end of the list.
-If the highest bit in the dir size is set, this points to the next
-metadata-pair in the current directory, otherwise it points to an arbitrary
-metadata-pair. Starting with the superblock, the tail-pointers form a
-linked-list containing all metadata-pairs in the filesystem.
-
-**CRC** - 32 bit CRC used to detect corruption from power-lost, from block
-end-of-life, or just from noise on the storage bus. The CRC is appended to
-the end of each metadata-block. The littlefs uses the standard CRC-32, which
-uses a polynomial of 0x04c11db7, initialized with 0xffffffff.
-
-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
-
-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"
-00000020: 08 00 06 07 00 00 00 06 00 00 00 63 6f 66 66 65  ...........coffe
-00000030: 65 22 08 00 04 09 00 00 00 08 00 00 00 73 6f 64  e"...........sod
-00000040: 61 22 08 00 05 1d 00 00 00 1c 00 00 00 6d 69 6c  a"...........mil
-00000050: 6b 31 22 08 00 05 1f 00 00 00 1e 00 00 00 6d 69  k1"...........mi
-00000060: 6c 6b 32 22 08 00 05 21 00 00 00 20 00 00 00 6d  lk2"...!... ...m
-00000070: 69 6c 6b 33 22 08 00 05 23 00 00 00 22 00 00 00  ilk3"...#..."...
-00000080: 6d 69 6c 6b 34 22 08 00 05 25 00 00 00 24 00 00  milk4"...%...$..
-00000090: 00 6d 69 6c 6b 35 06 31 6e c8                    .milk5.1n.
-```
-
-A note about the tail pointer linked-list: Normally, this linked-list is
-threaded through the entire filesystem. However, after power-loss this
-linked-list may become out of sync with the rest of the filesystem.
-- The linked-list may contain a directory that has actually been removed
-- The linked-list may contain a metadata pair that has not been updated after
-  a block in the pair has gone bad.
-
-The threaded linked-list must be checked for these errors before it can be
-used reliably. Fortunately, the threaded linked-list can simply be ignored
-if littlefs is mounted read-only.
-
-## Entries
-
-Each metadata block contains a series of entries that follow a standard
-layout. An entry contains the type of the entry, along with a section for
-entry-specific data, attributes, and a name.
-
-Here's the layout of entries on disk:
-
-| offset  | size                   | description                |
-|---------|------------------------|----------------------------|
-| 0x0     | 8 bits                 | entry type                 |
-| 0x1     | 8 bits                 | entry length               |
-| 0x2     | 8 bits                 | attribute length           |
-| 0x3     | 8 bits                 | name length                |
-| 0x4     | entry length bytes     | entry-specific data        |
-| 0x4+e   | attribute length bytes | system-specific attributes |
-| 0x4+e+a | name length bytes      | entry name                 |
-
-**Entry type** - Type of the entry, currently this is limited to the following:
-- 0x11 - file entry
-- 0x22 - directory entry
-- 0x2e - superblock entry
-
-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
-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.
-
-**Attribute length** - Length of system-specific attributes in bytes. Since
-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
-contain '/' and can not be '.' or '..' as these are a part of the syntax of
-filesystem paths.
-
-Here's an example of a simple entry stored on disk:
-```
-(8 bits)   entry type       = file     (0x11)
-(8 bits)   entry length     = 8 bytes  (0x08)
-(8 bits)   attribute length = 0 bytes  (0x00)
-(8 bits)   name length      = 12 bytes (0x0c)
-(8 bytes)  entry data       = 05 00 00 00 20 00 00 00
-(12 bytes) entry name       = smallavacado
-
-00000000: 11 08 00 0c 05 00 00 00 20 00 00 00 73 6d 61 6c  ........ ...smal
-00000010: 6c 61 76 61 63 61 64 6f                          lavacado
-```
-
-## Superblock
-
-The superblock is the anchor for the littlefs. The superblock is stored as
-a metadata pair containing a single superblock entry. It is through the
-superblock that littlefs can access the rest of the filesystem.
-
-The superblock can always be found in blocks 0 and 1, however fetching the
-superblock requires knowing the block size. The block size can be guessed by
-searching the beginning of disk for the string "littlefs", although currently
-the filesystems relies on the user providing the correct block size.
-
-The superblock is the most valuable block in the filesystem. It is updated
-very rarely, only during format or when the root directory must be moved. It
-is encouraged to always write out both superblock pairs even though it is not
-required.
-
-Here's the layout of the superblock entry:
-
-| offset | size                   | description                            |
-|--------|------------------------|----------------------------------------|
-| 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)                  |
-| 0x04   | 64 bits                | root directory                         |
-| 0x0c   | 32 bits                | block size                             |
-| 0x10   | 32 bits                | block count                            |
-| 0x14   | 32 bits                | version                                |
-| 0x18   | attribute length bytes | system-specific attributes             |
-| 0x18+a | 8 bytes                | magic string ("littlefs")              |
-
-**Root directory** - Pointer to the root directory's metadata pair.
-
-**Block size** - Size of the logical block size used by the filesystem.
-
-**Block count** - Number of blocks in the filesystem.
-
-**Version** - The littlefs version encoded as a 32 bit value. The upper 16 bits
-encodes the major version, which is incremented when a breaking-change is
-introduced in the filesystem specification. The lower 16 bits encodes the
-minor version, which is incremented when a backwards-compatible change is
-introduced. Non-standard Attribute changes do not change the version. This
-specification describes version 1.1 (0x00010001), which is the first version
-of littlefs.
-
-**Magic string** - The magic string "littlefs" takes the place of an entry
-name.
-
-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           (0x2e)
-(8 bits)  entry length     = 20 bytes             (0x14)
-(8 bits)  attribute length = 0 bytes              (0x00)
-(8 bits)  name length      = 8 bytes              (0x08)
-(64 bits) root directory   = 3, 2                 (0x00000003, 0x00000002)
-(32 bits) block size       = 512 bytes            (0x00000200)
-(32 bits) block count      = 1024 blocks          (0x00000400)
-(32 bits) version          = 1.1                  (0x00010001)
-(8 bytes) magic string     = littlefs
-(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  ................
-00000020: 00 04 00 00 01 00 01 00 6c 69 74 74 6c 65 66 73  ........littlefs
-00000030: fa 74 0b c5                                      .t..
-```
-
-## Directory entries
-
-Directories are stored in entries with a pointer to the first metadata pair
-in the directory. Keep in mind that a directory may be composed of multiple
-metadata pairs connected by the tail pointer when the highest bit in the dir
-size is set.
+providing a backup during erase cycles in case power is lost. These two blocks
+are not necessarily sequential and may be anywhere on disk, so a "pointer" to a
+metadata pair is stored as two block pointers.
 
-Here's the layout of a directory entry:
-
-| offset | size                   | description                             |
-|--------|------------------------|-----------------------------------------|
-| 0x0    | 8 bits                 | entry type (0x22 for directory entries) |
-| 0x1    | 8 bits                 | entry length (8 bytes)                  |
-| 0x2    | 8 bits                 | attribute length                        |
-| 0x3    | 8 bits                 | name length                             |
-| 0x4    | 64 bits                | directory pointer                       |
-| 0xc    | attribute length bytes | system-specific attributes              |
-| 0xc+a  | name length bytes      | directory name                          |
+On top of this, each metadata block behaves as an appendable log, containing a
+variable number of commits. Commits can be appended to the metadata log in
+order to update the metadata without requiring an erase cycles. Note that
+successive commits may supersede the metadata in previous commits. Only the
+most recent metadata should be considered valid.
+
+The high-level layout of a metadata block is fairly simple:
+
+```
+  .---------------------------------------.
+.-|  revision count   |      entries      |  \
+| |-------------------+                   |  |
+| |                                       |  |
+| |                                       |  +-- 1st commit
+| |                                       |  |
+| |                   +-------------------|  |
+| |                   |        CRC        |  /
+| |-------------------+-------------------|
+| |                entries                |  \
+| |                                       |  |
+| |                                       |  +-- 2nd commit
+| |    +-------------------+--------------|  |
+| |    |        CRC        |    padding   |  /
+| |----+-------------------+--------------|
+| |                entries                |  \
+| |                                       |  |
+| |                                       |  +-- 3rd commit
+| |         +-------------------+---------|  |
+| |         |        CRC        |         |  /
+| |---------+-------------------+         |
+| |           unwritten storage           |  more commits
+| |                                       |       |
+| |                                       |       v
+| |                                       |
+| |                                       |
+| '---------------------------------------'
+'---------------------------------------'
+```
+
+Each metadata block contains a 32-bit revision count followed by a number of
+commits. Each commit contains a variable number of metadata entries followed
+by a 32-bit CRC.
+
+Note also that entries aren't necessarily word-aligned. This allows us to
+store metadata more compactly, however we can only write to addresses that are
+aligned to our program block size. This means each commit may have padding for
+alignment.
+
+Metadata block fields:
+
+1. **Revision count (32-bits)** - Incremented every erase cycle. If both blocks
+   contain valid commits, only the block with the most recent revision count
+   should be used. Sequence comparison must be used to avoid issues with
+   integer overflow.
+
+2. **CRC (32-bits)** - Detects corruption from power-loss or other write
+   issues.  Uses a CRC-32 with a polynomial of `0x04c11db7` initialized
+   with `0xffffffff`.
+
+Entries themselves are stored as a 32-bit tag followed by a variable length
+blob of data. But exactly how these tags are stored is a little bit tricky.
+
+Metadata blocks support both forward and backward iteration. In order to do
+this without duplicating the space for each tag, neighboring entries have their
+tags XORed together, starting with `0xffffffff`.
 
-**Directory pointer** - Pointer to the first metadata pair in the directory.
-
-Here's an example of a directory entry:
 ```
-(8 bits)  entry type        = directory (0x22)
-(8 bits)  entry length      = 8 bytes   (0x08)
-(8 bits)  attribute length  = 0 bytes   (0x00)
-(8 bits)  name length       = 3 bytes   (0x03)
-(64 bits) directory pointer = 5, 4      (0x00000005, 0x00000004)
-(3 bytes) name              = tea
-
-00000000: 22 08 00 03 05 00 00 00 04 00 00 00 74 65 61     "...........tea
-```
-
-## File entries
-
-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 skip-list that is being referenced.
-
-How files are actually stored on disk is a bit complicated. The full
-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 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
+ Forward iteration                        Backward iteration
+
+.-------------------.  0xffffffff        .-------------------.
+|  revision count   |      |             |  revision count   |
+|-------------------|      v             |-------------------|
+|      tag ~A       |---> xor -> tag A   |      tag ~A       |---> xor -> 0xffffffff
+|-------------------|      |             |-------------------|      ^
+|       data A      |      |             |       data A      |      |
+|                   |      |             |                   |      |
+|                   |      |             |                   |      |
+|-------------------|      v             |-------------------|      |
+|      tag AxB      |---> xor -> tag B   |      tag AxB      |---> xor -> tag A
+|-------------------|      |             |-------------------|      ^
+|       data B      |      |             |       data B      |      |
+|                   |      |             |                   |      |
+|                   |      |             |                   |      |
+|-------------------|      v             |-------------------|      |
+|      tag BxC      |---> xor -> tag C   |      tag BxC      |---> xor -> tag B
+|-------------------|                    |-------------------|      ^
+|       data C      |                    |       data C      |      |
+|                   |                    |                   |    tag C
+|                   |                    |                   |
+|                   |                    |                   |
+'-------------------'                    '-------------------'
+```
+
+One last thing to note before we get into the details around tag encoding. Each
+tag contains a valid bit used to indicate if the tag and containing commit is
+valid. This valid bit is the first bit found in the tag and the commit and can
+be used to tell if we've attempted to write to the remaining space 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 a more complete example of metadata block containing 4 entries:
 
-Here's the layout of a file entry:
+```
+  .---------------------------------------.
+.-|  revision count   |      tag ~A       |        \
+| |-------------------+-------------------|        |
+| |                 data A                |        |
+| |                                       |        |
+| |-------------------+-------------------|        |
+| |      tag AxB      |       data B      | <--.   |
+| |-------------------+                   |    |   |
+| |                                       |    |   +-- 1st commit
+| |         +-------------------+---------|    |   |
+| |         |      tag BxC      |         | <-.|   |
+| |---------+-------------------+         |   ||   |
+| |                 data C                |   ||   |
+| |                                       |   ||   |
+| |-------------------+-------------------|   ||   |
+| |     tag CxCRC     |        CRC        |   ||   /
+| |-------------------+-------------------|   ||
+| |     tag CRCxA'    |      data A'      |   ||   \
+| |-------------------+                   |   ||   |
+| |                                       |   ||   |
+| |              +-------------------+----|   ||   +-- 2nd commit
+| |              |     tag CRCxA'    |    |   ||   |
+| |--------------+-------------------+----|   ||   |
+| | CRC          |        padding         |   ||   /
+| |--------------+----+-------------------|   ||
+| |     tag CRCxA''   |      data A''     | <---.  \
+| |-------------------+                   |   |||  |
+| |                                       |   |||  |
+| |         +-------------------+---------|   |||  |
+| |         |     tag A''xD     |         | < |||  |
+| |---------+-------------------+         |  ||||  +-- 3rd commit
+| |                data D                 |  ||||  |
+| |                             +---------|  ||||  |
+| |                             |   tag Dx|  ||||  |
+| |---------+-------------------+---------|  ||||  |
+| |CRC      |        CRC        |         |  ||||  /
+| |---------+-------------------+         |  ||||
+| |           unwritten storage           |  ||||  more commits
+| |                                       |  ||||       |
+| |                                       |  ||||       v
+| |                                       |  ||||
+| |                                       |  ||||
+| '---------------------------------------'  ||||
+'---------------------------------------'    |||'- most recent A
+                                             ||'-- most recent B
+                                             |'--- most recent C
+                                             '---- most recent D
+```
 
-| offset | size                   | description                        |
-|--------|------------------------|------------------------------------|
-| 0x0    | 8 bits                 | entry type (0x11 for file entries) |
-| 0x1    | 8 bits                 | entry length (8 bytes)             |
-| 0x2    | 8 bits                 | attribute length                   |
-| 0x3    | 8 bits                 | name length                        |
-| 0x4    | 32 bits                | file head                          |
-| 0x8    | 32 bits                | file size                          |
-| 0xc    | attribute length bytes | system-specific attributes         |
-| 0xc+a  | name length bytes      | directory name                     |
+## Metadata tags
 
-**File head** - Pointer to the block that is the head of the file's CTZ
-skip-list.
+So in littlefs, 32-bit tags describe every type of metadata. And this means
+_every_ type of metadata, including file entries, directory fields, and
+global state. Even the CRCs used to mark the end of commits get their own tag.
 
-**File size** - Size of file in bytes.
+Because of this, the tag format contains some densely packed information. Note
+that there are multiple levels of types which break down into more info:
 
-Here's an example of a file entry:
 ```
-(8 bits)   entry type       = file     (0x11)
-(8 bits)   entry length     = 8 bytes  (0x08)
-(8 bits)   attribute length = 0 bytes  (0x00)
-(8 bits)   name length      = 12 bytes (0x03)
-(32 bits)  file head        = 543      (0x0000021f)
-(32 bits)  file size        = 256 KB   (0x00040000)
-(12 bytes) name             = largeavacado
+[----            32             ----]
+[1|--  11   --|--  10  --|--  10  --]
+ ^.     ^     .     ^          ^- length
+ |.     |     .     '------------ id
+ |.     '-----.------------------ type (type3)
+ '.-----------.------------------ valid bit
+  [-3-|-- 8 --]
+    ^     ^- chunk
+    '------- type (type1)
+```
+
+
+Before we go further, there's one important thing to note. These tags are
+**not** stored in little-endian. Tags stored in commits are actually stored
+in big-endian (and is the only thing in littlefs stored in big-endian). This
+little bit of craziness comes from the fact that the valid bit must be the
+first bit in a commit, and when converted to little-endian, the valid bit finds
+itself in byte 4. We could restructure the tag to store the valid bit lower,
+but, because none of the fields are byte-aligned, this would be more
+complicated than just storing the tag in big-endian.
+
+Another thing to note is that both the tags `0x00000000` and `0xffffffff` are
+invalid and can be used for null values.
+
+Metadata tag fields:
+
+1. **Valid bit (1-bit)** - Indicates if the tag is valid.
+
+2. **Type3 (11-bits)** - Type of the tag. This field is broken down further
+   into a 3-bit abstract type and an 8-bit chunk field. Note that the value
+   `0x000` is invalid and not assigned a type.
 
-00000000: 11 08 00 0c 1f 02 00 00 00 00 04 00 6c 61 72 67  ............larg
-00000010: 65 61 76 61 63 61 64 6f                          eavacado
+3. **Type1 (3-bits)** - Abstract type of the tag. Groups the tags into
+   8 categories that facilitate bitmasked lookups.
+
+4. **Chunk (8-bits)** - Chunk field used for various purposes by the different
+   abstract types.  type1+chunk+id form a unique identifier for each tag in the
+   metadata block.
+
+5. **Id (10-bits)** - File id associated with the tag. Each file in a metadata
+   block gets a unique id which is used to associate tags with that file. The
+   special value `0x3ff` is used for any tags that are not associated with a
+   file, such as directory and global metadata.
+
+6. **Length (10-bits)** - Length of the data in bytes. The special value
+   `0x3ff` indicates that this tag has been deleted.
+
+## Metadata types
+
+What follows is an exhaustive list of metadata in littlefs.
+
+---
+#### `0x401` LFS_TYPE_CREATE
+
+Creates a new file with this id. Note that files in a metadata block
+don't necessarily need a create tag. All a create does is move over any
+files using this id. In this sense a create is similar to insertion into
+an imaginary array of files.
+
+The create and delete tags allow littlefs to keep files in a directory
+ordered alphabetically by filename.
+
+---
+#### `0x4ff` LFS_TYPE_DELETE
+
+Deletes the file with this id. An inverse to create, this tag moves over
+any files neighboring this id similar to a deletion from an imaginary
+array of files.
+
+---
+#### `0x0xx` LFS_TYPE_NAME
+
+Associates the id with a file name and file type.
+
+The data contains the file name stored as an ASCII string (may be expanded to
+UTF8 in the future).
+
+The chunk field in this tag indicates an 8-bit file type which can be one of
+the following.
+
+Currently, the name tag must precede any other tags associated with the id and
+can not be reassigned without deleting the file.
+
+Layout of the name tag:
+
+```
+        tag                          data
+[--      32      --][---        variable length        ---]
+[1| 3| 8 | 10 | 10 ][---            (size)             ---]
+ ^  ^  ^    ^    ^- size               ^- file name
+ |  |  |    '------ id
+ |  |  '----------- file type
+ |  '-------------- type1 (0x0)
+ '----------------- valid bit
 ```
 
-## Entry attributes
+Name fields:
+
+1. **file type (8-bits)** - Type of the file.
+
+2. **file name** - File name stored as an ASCII string.
+
+---
+#### `0x001` LFS_TYPE_REG
 
-Each dir entry can have up to 256 bytes of system-specific attributes. Since
-these attributes are system-specific, they may not be portable between
-different systems. For this reason, all attributes must be optional. A minimal
-littlefs driver must be able to get away with supporting no attributes at all.
+Initializes the id + name as a regular file.
 
-For some level of portability, littlefs has a simple scheme for attributes.
-Each attribute is prefixes with an 8-bit type that indicates what the attribute
-is. The length of attributes may also be determined from this type. Attributes
-in an entry should be sorted based on portability, since attribute parsing
-will end when it hits the first attribute it does not understand.
+How each file is stored depends on its struct tag, which is described below.
 
-Each system should choose a 4-bit value to prefix all attribute types with to
-avoid conflicts with other systems. Additionally, littlefs drivers that support
-attributes should provide a "ignore attributes" flag to users in case attribute
-conflicts do occur.
+---
+#### `0x002` LFS_TYPE_DIR
 
-Attribute types prefixes with 0x0 and 0xf are currently reserved for future
-standard attributes. Standard attributes will be added to this document in
-that case.
+Initializes the id + name as a directory.
 
-Here's an example of non-standard time attribute:
+Directories in littlefs are stored on disk as a linked-list of metadata pairs,
+each pair containing any number of files in alphabetical order. A pointer to
+the directory is stored in the struct tag, which is described below.
+
+---
+#### `0x0ff` LFS_TYPE_SUPERBLOCK
+
+Initializes the id as a superblock entry.
+
+The superblock entry is a special entry used to store format-time configuration
+and identify the filesystem.
+
+The name is a bit of a misnomer. While the superblock entry serves the same
+purpose as a superblock found in other filesystems, in littlefs the superblock
+does not get a dedicated block. Instead, the superblock entry is duplicated
+across a linked-list of metadata pairs rooted on the blocks 0 and 1. The last
+metadata pair doubles as the root directory of the filesystem.
+
+```
+ .--------.  .--------.  .--------.  .--------.  .--------.
+.| super  |->| super  |->| super  |->| super  |->| file B |
+|| block  | || block  | || block  | || block  | || file C |
+||        | ||        | ||        | || file A | || file D |
+|'--------' |'--------' |'--------' |'--------' |'--------'
+'--------'  '--------'  '--------'  '--------'  '--------'
+
+\----------------+----------------/ \----------+----------/
+          superblock pairs               root directory
 ```
-(8 bits)  attribute type  = time       (0xc1)
-(72 bits) time in seconds = 1506286115 (0x0059c81a23)
 
-00000000: c1 23 1a c8 59 00                                .#..Y.
+The filesystem starts with only the root directory. The superblock metadata
+pairs grow every time the root pair is compacted in order to prolong the
+life of the device exponentially.
+
+The contents of the superblock entry are stored in a name tag with the
+superblock type and an inline-struct tag. The name tag contains the magic
+string "littlefs", while the inline-struct tag contains version and
+configuration information.
+
+Layout of the superblock name tag and inline-struct tag:
+
 ```
+        tag                          data
+[--      32      --][--      32      --|--      32      --]
+[1|- 11 -| 10 | 10 ][---              64               ---]
+ ^    ^     ^    ^- size (8)           ^- magic string ("littlefs")
+ |    |     '------ id (0)
+ |    '------------ type (0x0ff)
+ '----------------- valid bit
+
+        tag                          data
+[--      32      --][--      32      --|--      32      --|--      32      --]
+[1|- 11 -| 10 | 10 ][--      32      --|--      32      --|--      32      --]
+ ^    ^     ^    ^            ^- version         ^- block size      ^- block count
+ |    |     |    |  [--      32      --|--      32      --|--      32      --]
+ |    |     |    |  [--      32      --|--      32      --|--      32      --]
+ |    |     |    |            ^- name max        ^- file max        ^- attr max
+ |    |     |    '- size (24)
+ |    |     '------ id (0)
+ |    '------------ type (0x201)
+ '----------------- valid bit
+```
+
+Superblock fields:
+
+1. **Magic string (8-bytes)** - Magic string indicating the presence of
+   littlefs on the device. Must be the string "littlefs".
+
+2. **Version (32-bits)** - The version of littlefs at format time. The version
+   is encoded in a 32-bit value with the upper 16-bits containing the major
+   version, and the lower 16-bits containing the minor version.
+
+   This specification describes version 2.0 (`0x00020000`).
+
+3. **Block size (32-bits)** - Size of the logical block size used by the
+   filesystem in bytes.
+
+4. **Block count (32-bits)** - Number of blocks in the filesystem.
+
+5. **Name max (32-bits)** - Maximum size of file names in bytes.
+
+6. **File max (32-bits)** - Maximum size of files in bytes.
+
+7. **Attr max (32-bits)** - Maximum size of file attributes in bytes.
+
+The superblock must always be the first entry (id 0) in a metadata pair as well
+as be the first entry written to the block. This means that the superblock
+entry can be read from a device using offsets alone.
+
+---
+#### `0x2xx` LFS_TYPE_STRUCT
+
+Associates the id with an on-disk data structure.
 
-Here's an example of non-standard permissions attribute:
+The exact layout of the data depends on the data structure type stored in the
+chunk field and can be one of the following.
+
+Any type of struct supersedes all other structs associated with the id. For
+example, appending a ctz-struct replaces an inline-struct on the same file.
+
+---
+#### `0x200` LFS_TYPE_DIRSTRUCT
+
+Gives the id a directory data structure.
+
+Directories in littlefs are stored on disk as a linked-list of metadata pairs,
+each pair containing any number of files in alphabetical order.
+
+```
+     |
+     v
+ .--------.  .--------.  .--------.  .--------.  .--------.  .--------.
+.| file A |->| file D |->| file G |->| file I |->| file J |->| file M |
+|| file B | || file E | || file H | ||        | || file K | || file N |
+|| file C | || file F | ||        | ||        | || file L | ||        |
+|'--------' |'--------' |'--------' |'--------' |'--------' |'--------'
+'--------'  '--------'  '--------'  '--------'  '--------'  '--------'
 ```
-(8 bits)  attribute type  = permissions (0xc2)
-(16 bits) permission bits = rw-rw-r--   (0x01b4)
 
-00000000: c2 b4 01                                         ...
+The dir-struct tag contains only the pointer to the first metadata-pair in the
+directory. The directory size is not known without traversing the directory.
+
+The pointer to the next metadata-pair in the directory is stored in a tail tag,
+which is described below.
+
+Layout of the dir-struct tag:
+
 ```
+        tag                          data
+[--      32      --][--      32      --|--      32      --]
+[1|- 11 -| 10 | 10 ][---              64               ---]
+ ^    ^     ^    ^- size (8)           ^- metadata pair
+ |    |     '------ id
+ |    '------------ type (0x200)
+ '----------------- valid bit
+```
+
+Dir-struct fields:
+
+1. **Metadata pair (8-bytes)** - Pointer to the first metadata-pair
+   in the directory.
+
+---
+#### `0x201` LFS_TYPE_INLINESTRUCT
+
+Gives the id an inline data structure.
+
+Inline structs store small files that can fit in the metadata pair. In this
+case, the file data is stored directly in the tag's data area.
+
+Layout of the inline-struct tag:
 
-Here's what a dir entry may look like with these attributes:
 ```
-(8 bits)   entry type       = file         (0x11)
-(8 bits)   entry length     = 8 bytes      (0x08)
-(8 bits)   attribute length = 9 bytes      (0x09)
-(8 bits)   name length      = 12 bytes     (0x0c)
-(8 bytes)  entry data       = 05 00 00 00 20 00 00 00
-(8 bits)   attribute type   = time         (0xc1)
-(72 bits)  time in seconds  = 1506286115   (0x0059c81a23)
-(8 bits)   attribute type   = permissions  (0xc2)
-(16 bits)  permission bits  = rw-rw-r--    (0x01b4)
-(12 bytes) entry name       = smallavacado
+        tag                          data
+[--      32      --][---        variable length        ---]
+[1|- 11 -| 10 | 10 ][---            (size)             ---]
+ ^    ^     ^    ^- size               ^- inline data
+ |    |     '------ id
+ |    '------------ type (0x201)
+ '----------------- valid bit
+```
+
+Inline-struct fields:
+
+1. **Inline data** - File data stored directly in the metadata-pair.
+
+---
+#### `0x202` LFS_TYPE_CTZSTRUCT
+
+Gives the id a CTZ skip-list data structure.
+
+CTZ skip-lists store files that can not fit in the metadata pair. These files
+are stored in a skip-list in reverse, with a pointer to the head of the
+skip-list. Note that the head of the skip-list and the file size is enough
+information to read the file.
+
+How exactly CTZ skip-lists work is a bit complicated. A full explanation can be
+found in the [DESIGN.md](DESIGN.md#ctz-skip-lists).
+
+A quick summary: For every _n_&zwj;th block where _n_ is divisible by
+2&zwj;_&#739;_, that block contains a pointer to block _n_-2&zwj;_&#739;_.
+These pointers are stored in increasing order of _x_ in each block of the file
+before the actual data.
+
+```
+                                                               |
+                                                               v
+.--------.  .--------.  .--------.  .--------.  .--------.  .--------.
+| A      |<-| D      |<-| G      |<-| J      |<-| M      |<-| P      |
+| B      |<-| E      |--| H      |<-| K      |--| N      |  | Q      |
+| C      |<-| F      |--| I      |--| L      |--| O      |  |        |
+'--------'  '--------'  '--------'  '--------'  '--------'  '--------'
+  block 0     block 1     block 2     block 3     block 4     block 5
+              1 skip      2 skips     1 skip      3 skips     1 skip
+```
+
+Note that 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.
+
+Layout of the CTZ-struct tag:
+
+```
+        tag                          data
+[--      32      --][--      32      --|--      32      --]
+[1|- 11 -| 10 | 10 ][--      32      --|--      32      --]
+ ^    ^     ^    ^            ^                  ^- file size
+ |    |     |    |            '-------------------- file head
+ |    |     |    '- size (8)
+ |    |     '------ id
+ |    '------------ type (0x202)
+ '----------------- valid bit
+```
+
+CTZ-struct fields:
+
+1. **File head (32-bits)** - Pointer to the block that is the head of the
+   file's CTZ skip-list.
+
+2. **File size (32-bits)** - Size of the file in bytes.
+
+---
+#### `0x3xx` LFS_TYPE_USERATTR
+
+Attaches a user attribute to an id.
+
+littlefs has a concept of "user attributes". These are small user-provided
+attributes that can be used to store things like timestamps, hashes,
+permissions, etc.
+
+Each user attribute is uniquely identified by an 8-bit type which is stored in
+the chunk field, and the user attribute itself can be found in the tag's data.
+
+There are currently no standard user attributes and a portable littlefs
+implementation should work with any user attributes missing.
+
+Layout of the user-attr tag:
 
-00000000: 11 08 09 0c 05 00 00 00 20 00 00 00 c1 23 1a c8  ........ ....#..
-00000010: 59 00 c2 b4 01 73 6d 61 6c 6c 61 76 61 63 61 64  Y....smallavacad
-00000020: 6f                                               o
 ```
+        tag                          data
+[--      32      --][---        variable length        ---]
+[1| 3| 8 | 10 | 10 ][---            (size)             ---]
+ ^  ^  ^    ^    ^- size               ^- attr data
+ |  |  |    '------ id
+ |  |  '----------- attr type
+ |  '-------------- type1 (0x3)
+ '----------------- valid bit
+```
+
+User-attr fields:
+
+1. **Attr type (8-bits)** - Type of the user attributes.
+
+2. **Attr data** - The data associated with the user attribute.
+
+---
+#### `0x6xx` LFS_TYPE_TAIL
+
+Provides the tail pointer for the metadata pair itself.
+
+The metadata pair's tail pointer is used in littlefs for a linked-list
+containing all metadata pairs. The chunk field contains the type of the tail,
+which indicates if the following metadata pair is a part of the directory
+(hard-tail) or only used to traverse the filesystem (soft-tail).
+
+```
+         .--------.
+        .| dir A  |-.
+        ||softtail| |
+.--------|        |-'
+|       |'--------'
+|       '---|--|-'
+|        .-'    '-------------.
+|       v                      v
+|  .--------.  .--------.  .--------.
+'->| dir B  |->| dir B  |->| dir C  |
+  ||hardtail| ||softtail| ||        |
+  ||        | ||        | ||        |
+  |'--------' |'--------' |'--------'
+  '--------'  '--------'  '--------'
+```
+
+Currently any type supersedes any other preceding tails in the metadata pair,
+but this may change if additional metadata pair state is added.
+
+A note about the metadata pair linked-list: Normally, this linked-list contains
+every metadata pair in the filesystem. However, there are some operations that
+can cause this linked-list to become out of sync if a power-loss were to occur.
+When this happens, littlefs sets the "sync" flag in the global state. How
+exactly this flag is stored is described below.
+
+When the sync flag is set:
+
+1. The linked-list may contain an orphaned directory that has been removed in
+   the filesystem.
+2. The linked-list may contain a metadata pair with a bad block that has been
+   replaced in the filesystem.
+
+If the sync flag is set, the threaded linked-list must be checked for these
+errors before it can be used reliably. Note that the threaded linked-list can
+be ignored if littlefs is mounted read-only.
+
+Layout of the tail tag:
+
+```
+        tag                          data
+[--      32      --][--      32      --|--      32      --]
+[1| 3| 8 | 10 | 10 ][---              64               ---]
+ ^  ^  ^   ^    ^- size (8)            ^- metadata pair
+ |  |  |   '------ id
+ |  |  '---------- tail type
+ |  '------------- type1 (0x6)
+ '---------------- valid bit
+```
+
+Tail fields:
+
+1. **Tail type (8-bits)** - Type of the tail pointer.
+
+2. **Metadata pair (8-bytes)** - Pointer to the next metadata-pair.
+
+---
+#### `0x600` LFS_TYPE_SOFTTAIL
+
+Provides a tail pointer that points to the next metadata pair in the
+filesystem.
+
+In this case, the next metadata pair is not a part of our current directory
+and should only be followed when traversing the entire filesystem.
+
+---
+#### `0x601` LFS_TYPE_HARDTAIL
+
+Provides a tail pointer that points to the next metadata pair in the
+directory.
+
+In this case, the next metadata pair belongs to the current directory. Note
+that because directories in littlefs are sorted alphabetically, the next
+metadata pair should only contain filenames greater than any filename in the
+current pair.
+
+---
+#### `0x7xx` LFS_TYPE_GSTATE
+
+Provides delta bits for global state entries.
+
+littlefs has a concept of "global state". This is a small set of state that
+can be updated by a commit to _any_ metadata pair in the filesystem.
+
+The way this works is that the global state is stored as a set of deltas
+distributed across the filesystem such that the global state can be found by
+the xor-sum of these deltas.
+
+```
+ .--------.  .--------.  .--------.  .--------.  .--------.
+.|        |->| gdelta |->|        |->| gdelta |->| gdelta |
+||        | || 0x23   | ||        | || 0xff   | || 0xce   |
+||        | ||        | ||        | ||        | ||        |
+|'--------' |'--------' |'--------' |'--------' |'--------'
+'--------'  '----|---'  '--------'  '----|---'  '----|---'
+                 v                       v           v
+       0x00 --> xor ------------------> xor ------> xor --> gstate = 0x12
+```
+
+Note that storing globals this way is very expensive in terms of storage usage,
+so any global state should be kept very small.
+
+The size and format of each piece of global state depends on the type, which
+is stored in the chunk field. Currently, the only global state is move state,
+which is outlined below.
+
+---
+#### `0x7ff` LFS_TYPE_MOVESTATE
+
+Provides delta bits for the global move state.
+
+The move state in littlefs is used to store info about operations that could
+cause to filesystem to go out of sync if the power is lost. The operations
+where this could occur is moves of files between metadata pairs and any
+operation that changes metadata pairs on the threaded linked-list.
+
+In the case of moves, the move state contains a tag + metadata pair describing
+the source of the ongoing move. If this tag is non-zero, that means that power
+was lost during a move, and the file exists in two different locations. If this
+happens, the source of the move should be considered deleted, and the move
+should be completed (the source should be deleted) before any other write
+operations to the filesystem.
+
+In the case of operations to the threaded linked-list, a single "sync" bit is
+used to indicate that a modification is ongoing. If this sync flag is set, the
+threaded linked-list will need to be checked for errors before it can be used
+reliably. The exact cases to check for are described above in the tail tag.
+
+Layout of the move state:
+
+```
+        tag                                    data
+[--      32      --][--      32      --|--      32      --|--      32      --]
+[1|- 11 -| 10 | 10 ][1|- 11 -| 10 | 10 |---              64               ---]
+ ^    ^     ^    ^   ^    ^     ^    ^- padding (0)       ^- metadata pair
+ |    |     |    |   |    |     '------ move id
+ |    |     |    |   |    '------------ move type
+ |    |     |    |   '----------------- sync bit
+ |    |     |    |
+ |    |     |    '- size (12)
+ |    |     '------ id (0x3ff)
+ |    '------------ type (0x7ff)
+ '----------------- valid bit
+```
+
+Move state fields:
+
+1. **Sync bit (1-bit)** - Indicates if the metadata pair threaded linked-list
+   is in-sync. If set, the threaded linked-list should be checked for errors.
+
+2. **Move type (11-bits)** - Type of move being performed. Must be either
+   `0x000`, indicating no move, or `0x4ff` indicating the source file should
+   be deleted.
+
+3. **Move id (10-bits)** - The file id being moved.
+
+4. **Metadata pair (8-bytes)** - Pointer to the metadata-pair containing
+   the move.
+
+---
+#### `0x5xx` LFS_TYPE_CRC
+
+Last but not least, the CRC tag marks the end of a commit and provides a
+checksum for any commits to the metadata block.
+
+The first 32-bits of the data contain a CRC-32 with a polynomial of
+`0x04c11db7` initialized with `0xffffffff`. This CRC provides a checksum for
+all metadata since the previous CRC tag, including the CRC tag itself. For
+the first commit, this includes the revision count for the metadata block.
+
+However, the size of the data is not limited to 32-bits. The data field may
+larger to pad the commit to the next program-aligned boundary.
+
+In addition, the CRC tag's chunk field contains a set of flags which can
+change the behaviour of commits. Currently the only flag in use is the lowest
+bit, which determines the expected state of the valid bit for any following
+tags. This is used to guarantee that unwritten storage in a metadata block
+will be detected as invalid.
+
+Layout of the CRC tag:
+
+```
+        tag                                    data
+[--      32      --][--      32      --|---        variable length        ---]
+[1| 3| 8 | 10 | 10 ][--      32      --|---            (size)             ---]
+ ^  ^  ^    ^    ^            ^- crc                      ^- padding
+ |  |  |    |    '- size (12)
+ |  |  |    '------ id (0x3ff)
+ |  |  '----------- valid state
+ |  '-------------- type1 (0x5)
+ '----------------- valid bit
+```
+
+CRC fields:
+
+1. **Valid state (1-bit)** - Indicates the expected value of the valid bit for
+   any tags in the next commit.
+
+2. **CRC (32-bits)** - CRC-32 with a polynomial of `0x04c11db7` initialized
+   with `0xffffffff`.
+
+3. **Padding** - Padding to the next program-aligned boundary. No guarantees
+   are made about the contents.
+
+---

+ 86 - 9
emubd/lfs_emubd.c

@@ -19,6 +19,40 @@
 #include <inttypes.h>
 
 
+// Emulated block device utils
+static inline void lfs_emubd_tole32(lfs_emubd_t *emu) {
+    emu->cfg.read_size     = lfs_tole32(emu->cfg.read_size);
+    emu->cfg.prog_size     = lfs_tole32(emu->cfg.prog_size);
+    emu->cfg.block_size    = lfs_tole32(emu->cfg.block_size);
+    emu->cfg.block_count   = lfs_tole32(emu->cfg.block_count);
+
+    emu->stats.read_count  = lfs_tole32(emu->stats.read_count);
+    emu->stats.prog_count  = lfs_tole32(emu->stats.prog_count);
+    emu->stats.erase_count = lfs_tole32(emu->stats.erase_count);
+
+    for (unsigned i = 0; i < sizeof(emu->history.blocks) /
+            sizeof(emu->history.blocks[0]); i++) {
+        emu->history.blocks[i] = lfs_tole32(emu->history.blocks[i]);
+    }
+}
+
+static inline void lfs_emubd_fromle32(lfs_emubd_t *emu) {
+    emu->cfg.read_size     = lfs_fromle32(emu->cfg.read_size);
+    emu->cfg.prog_size     = lfs_fromle32(emu->cfg.prog_size);
+    emu->cfg.block_size    = lfs_fromle32(emu->cfg.block_size);
+    emu->cfg.block_count   = lfs_fromle32(emu->cfg.block_count);
+
+    emu->stats.read_count  = lfs_fromle32(emu->stats.read_count);
+    emu->stats.prog_count  = lfs_fromle32(emu->stats.prog_count);
+    emu->stats.erase_count = lfs_fromle32(emu->stats.erase_count);
+
+    for (unsigned i = 0; i < sizeof(emu->history.blocks) /
+            sizeof(emu->history.blocks[0]); i++) {
+        emu->history.blocks[i] = lfs_fromle32(emu->history.blocks[i]);
+    }
+}
+
+
 // Block device emulated on existing filesystem
 int lfs_emubd_create(const struct lfs_config *cfg, const char *path) {
     lfs_emubd_t *emu = cfg->context;
@@ -46,17 +80,31 @@ int lfs_emubd_create(const struct lfs_config *cfg, const char *path) {
     }
 
     // Load stats to continue incrementing
-    snprintf(emu->child, LFS_NAME_MAX, "stats");
-
+    snprintf(emu->child, LFS_NAME_MAX, ".stats");
     FILE *f = fopen(emu->path, "r");
-    if (!f && errno != ENOENT) {
-        return -errno;
+    if (!f) {
+        memset(&emu->stats, 0, sizeof(emu->stats));
+    } else {
+        size_t res = fread(&emu->stats, sizeof(emu->stats), 1, f);
+        lfs_emubd_fromle32(emu);
+        if (res < 1) {
+            return -errno;
+        }
+
+        err = fclose(f);
+        if (err) {
+            return -errno;
+        }
     }
 
-    if (errno == ENOENT) {
-        memset(&emu->stats, 0x0, sizeof(emu->stats));
+    // Load history
+    snprintf(emu->child, LFS_NAME_MAX, ".history");
+    f = fopen(emu->path, "r");
+    if (!f) {
+        memset(&emu->history, 0, sizeof(emu->history));
     } else {
-        size_t res = fread(&emu->stats, sizeof(emu->stats), 1, f);
+        size_t res = fread(&emu->history, sizeof(emu->history), 1, f);
+        lfs_emubd_fromle32(emu);
         if (res < 1) {
             return -errno;
         }
@@ -166,6 +214,13 @@ int lfs_emubd_prog(const struct lfs_config *cfg, lfs_block_t block,
         return -errno;
     }
 
+    // update history and stats
+    if (block != emu->history.blocks[0]) {
+        memcpy(&emu->history.blocks[1], &emu->history.blocks[0],
+                sizeof(emu->history) - sizeof(emu->history.blocks[0]));
+        emu->history.blocks[0] = block;
+    }
+
     emu->stats.prog_count += 1;
     return 0;
 }
@@ -211,13 +266,15 @@ int lfs_emubd_sync(const struct lfs_config *cfg) {
     lfs_emubd_t *emu = cfg->context;
 
     // Just write out info/stats for later lookup
-    snprintf(emu->child, LFS_NAME_MAX, "config");
+    snprintf(emu->child, LFS_NAME_MAX, ".config");
     FILE *f = fopen(emu->path, "w");
     if (!f) {
         return -errno;
     }
 
+    lfs_emubd_tole32(emu);
     size_t res = fwrite(&emu->cfg, sizeof(emu->cfg), 1, f);
+    lfs_emubd_fromle32(emu);
     if (res < 1) {
         return -errno;
     }
@@ -227,13 +284,33 @@ int lfs_emubd_sync(const struct lfs_config *cfg) {
         return -errno;
     }
 
-    snprintf(emu->child, LFS_NAME_MAX, "stats");
+    snprintf(emu->child, LFS_NAME_MAX, ".stats");
     f = fopen(emu->path, "w");
     if (!f) {
         return -errno;
     }
 
+    lfs_emubd_tole32(emu);
     res = fwrite(&emu->stats, sizeof(emu->stats), 1, f);
+    lfs_emubd_fromle32(emu);
+    if (res < 1) {
+        return -errno;
+    }
+
+    err = fclose(f);
+    if (err) {
+        return -errno;
+    }
+
+    snprintf(emu->child, LFS_NAME_MAX, ".history");
+    f = fopen(emu->path, "w");
+    if (!f) {
+        return -errno;
+    }
+
+    lfs_emubd_tole32(emu);
+    res = fwrite(&emu->history, sizeof(emu->history), 1, f);
+    lfs_emubd_fromle32(emu);
     if (res < 1) {
         return -errno;
     }

+ 4 - 0
emubd/lfs_emubd.h

@@ -45,6 +45,10 @@ typedef struct lfs_emubd {
         uint64_t erase_count;
     } stats;
 
+    struct {
+        lfs_block_t blocks[4];
+    } history;
+
     struct {
         uint32_t read_size;
         uint32_t prog_size;

File diff suppressed because it is too large
+ 814 - 536
lfs.c


+ 283 - 138
lfs.h

@@ -21,14 +21,14 @@ extern "C"
 // Software library version
 // Major (top-nibble), incremented on backwards incompatible changes
 // Minor (bottom-nibble), incremented on feature additions
-#define LFS_VERSION 0x00010007
+#define LFS_VERSION 0x00020000
 #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 0x00020000
 #define LFS_DISK_VERSION_MAJOR (0xffff & (LFS_DISK_VERSION >> 16))
 #define LFS_DISK_VERSION_MINOR (0xffff & (LFS_DISK_VERSION >>  0))
 
@@ -44,57 +44,98 @@ typedef int32_t  lfs_soff_t;
 
 typedef uint32_t lfs_block_t;
 
-// Max name size in bytes
+// Maximum name size in bytes, may be redefined to reduce the size of the
+// info struct. Limited to <= 1022. Stored in superblock and must be
+// respected by other littlefs drivers.
 #ifndef LFS_NAME_MAX
 #define LFS_NAME_MAX 255
 #endif
 
-// Max file size in bytes
+// Maximum size of a file in bytes, may be redefined to limit to support other
+// drivers. Limited on disk to <= 4294967296. However, above 2147483647 the
+// functions lfs_file_seek, lfs_file_size, and lfs_file_tell will return
+// incorrect values due to using signed integers. Stored in superblock and
+// must be respected by other littlefs drivers.
 #ifndef LFS_FILE_MAX
 #define LFS_FILE_MAX 2147483647
 #endif
 
+// Maximum size of custom attributes in bytes, may be redefined, but there is
+// no real benefit to using a smaller LFS_ATTR_MAX. Limited to <= 1022.
+#ifndef LFS_ATTR_MAX
+#define LFS_ATTR_MAX 1022
+#endif
+
 // 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_NOTEMPTY = -39,  // Dir is not empty
-    LFS_ERR_BADF     = -9,   // Bad file number
-    LFS_ERR_FBIG     = -27,  // File too large
-    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     = -84,  // 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_FBIG        = -27,  // File too large
+    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_NOATTR      = -61,  // No data/attr available
+    LFS_ERR_NAMETOOLONG = -36,  // File name too long
 };
 
 // File types
 enum lfs_type {
-    LFS_TYPE_REG        = 0x11,
-    LFS_TYPE_DIR        = 0x22,
-    LFS_TYPE_SUPERBLOCK = 0x2e,
+    // file types
+    LFS_TYPE_REG            = 0x001,
+    LFS_TYPE_DIR            = 0x002,
+
+    // internally used types
+    LFS_TYPE_SPLICE         = 0x400,
+    LFS_TYPE_NAME           = 0x000,
+    LFS_TYPE_STRUCT         = 0x200,
+    LFS_TYPE_USERATTR       = 0x300,
+    LFS_TYPE_FROM           = 0x100,
+    LFS_TYPE_TAIL           = 0x600,
+    LFS_TYPE_GLOBALS        = 0x700,
+    LFS_TYPE_CRC            = 0x500,
+
+    // internally used type specializations
+    LFS_TYPE_CREATE         = 0x401,
+    LFS_TYPE_DELETE         = 0x4ff,
+    LFS_TYPE_SUPERBLOCK     = 0x0ff,
+    LFS_TYPE_DIRSTRUCT      = 0x200,
+    LFS_TYPE_CTZSTRUCT      = 0x202,
+    LFS_TYPE_INLINESTRUCT   = 0x201,
+    LFS_TYPE_SOFTTAIL       = 0x600,
+    LFS_TYPE_HARDTAIL       = 0x601,
+    LFS_TYPE_MOVESTATE      = 0x7ff,
+
+    // internal chip sources
+    LFS_FROM_NOOP           = 0x000,
+    LFS_FROM_MOVE           = 0x101,
+    LFS_FROM_USERATTRS      = 0x102,
 };
 
 // File open flags
 enum lfs_open_flags {
     // open flags
-    LFS_O_RDONLY = 1,        // Open a file as read only
-    LFS_O_WRONLY = 2,        // Open a file as write only
-    LFS_O_RDWR   = 3,        // Open a file as read and write
-    LFS_O_CREAT  = 0x0100,   // Create a file if it does not exist
-    LFS_O_EXCL   = 0x0200,   // Fail if a file already exists
-    LFS_O_TRUNC  = 0x0400,   // Truncate the existing file to zero size
-    LFS_O_APPEND = 0x0800,   // Move to end of file on every write
+    LFS_O_RDONLY = 1,         // Open a file as read only
+    LFS_O_WRONLY = 2,         // Open a file as write only
+    LFS_O_RDWR   = 3,         // Open a file as read and write
+    LFS_O_CREAT  = 0x0100,    // Create a file if it does not exist
+    LFS_O_EXCL   = 0x0200,    // Fail if a file already exists
+    LFS_O_TRUNC  = 0x0400,    // Truncate the existing file to zero size
+    LFS_O_APPEND = 0x0800,    // Move to end of file on every write
 
     // internally used 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
+    LFS_F_DIRTY   = 0x010000, // File does not match storage
+    LFS_F_WRITING = 0x020000, // File has been written since last flush
+    LFS_F_READING = 0x040000, // File has been read since last flush
+    LFS_F_ERRED   = 0x080000, // An error occured during write
+    LFS_F_INLINE  = 0x100000, // Currently inlined in directory entry
 };
 
 // File seek flags
@@ -132,52 +173,68 @@ struct lfs_config {
     // are propogated to the user.
     int (*sync)(const struct lfs_config *c);
 
-    // Minimum size of a block read. This determines the size of read buffers.
-    // This may be larger than the physical read size to improve performance
-    // by caching more of the block device.
+    // Minimum size of a block read. All read operations will be a
+    // multiple of this value.
     lfs_size_t read_size;
 
-    // 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.
+    // Minimum size of a block program. All program operations will be a
+    // multiple of this value.
     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.
-    // Must be a multiple of the program size.
+    // may be larger than the physical erase size. However, non-inlined files
+    // take up at minimum one block. Must be a multiple of the read
+    // and program sizes.
     lfs_size_t block_size;
 
     // Number of erasable blocks on the device.
     lfs_size_t block_count;
 
-    // Number of blocks to lookahead during block allocation. A larger
-    // lookahead reduces the number of passes required to allocate a block.
-    // The lookahead buffer requires only 1 bit per block so it can be quite
-    // large with little ram impact. Should be a multiple of 32.
-    lfs_size_t lookahead;
-
-    // Optional, statically allocated read buffer. Must be read sized.
+    // Number of erase cycles before we should move data to another block.
+    // May be zero, in which case no block-level wear-leveling is performed.
+    uint32_t block_cycles;
+
+    // Size of block caches. Each cache buffers a portion of a block in RAM.
+    // The littlefs needs a read cache, a program cache, and one additional
+    // cache per file. Larger caches can improve performance by storing more
+    // data and reducing the number of disk accesses. Must be a multiple of
+    // the read and program sizes, and a factor of the block size.
+    lfs_size_t cache_size;
+
+    // Size of the lookahead buffer in bytes. A larger lookahead buffer
+    // increases the number of blocks found during an allocation pass. The
+    // lookahead buffer is stored as a compact bitmap, so each byte of RAM
+    // can track 8 blocks. Must be a multiple of 4.
+    lfs_size_t lookahead_size;
+
+    // Optional statically allocated read buffer. Must be cache_size.
+    // By default lfs_malloc is used to allocate this buffer.
     void *read_buffer;
 
-    // Optional, statically allocated program buffer. Must be program sized.
+    // Optional statically allocated program buffer. Must be cache_size.
+    // By default lfs_malloc is used to allocate this buffer.
     void *prog_buffer;
 
-    // Optional, statically allocated lookahead buffer. Must be 1 bit per
-    // lookahead block.
+    // Optional statically allocated lookahead buffer. Must be lookahead_size
+    // and aligned to a 64-bit boundary. By default lfs_malloc is used to
+    // allocate this buffer.
     void *lookahead_buffer;
 
-    // Optional, statically allocated buffer for files. Must be program sized.
-    // If enabled, only one file may be opened at a time.
-    void *file_buffer;
-};
-
-// Optional configuration provided during lfs_file_opencfg
-struct lfs_file_config {
-    // Optional, statically allocated buffer for files. Must be program sized.
-    // If NULL, malloc will be used by default.
-    void *buffer;
+    // Optional upper limit on length of file names in bytes. No downside for
+    // larger names except the size of the info struct which is controlled by
+    // the LFS_NAME_MAX define. Defaults to LFS_NAME_MAX when zero. Stored in
+    // superblock and must be respected by other littlefs drivers.
+    lfs_size_t name_max;
+
+    // Optional upper limit on files in bytes. No downside for larger files
+    // but must be <= LFS_FILE_MAX. Defaults to LFS_FILE_MAX when zero. Stored
+    // in superblock and must be respected by other littlefs drivers.
+    lfs_size_t file_max;
+
+    // Optional upper limit on custom attributes in bytes. No downside for
+    // larger attributes size but must be <= LFS_ATTR_MAX. Defaults to
+    // LFS_ATTR_MAX when zero.
+    lfs_size_t attr_max;
 };
 
 // File info structure
@@ -185,108 +242,149 @@ struct lfs_info {
     // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR
     uint8_t type;
 
-    // Size of the file, only valid for REG files
+    // Size of the file, only valid for REG files. Limited to 32-bits.
     lfs_size_t size;
 
-    // Name of the file stored as a null-terminated string
+    // Name of the file stored as a null-terminated string. Limited to
+    // LFS_NAME_MAX+1, which can be changed by redefining LFS_NAME_MAX to
+    // reduce RAM. LFS_NAME_MAX is stored in superblock and must be
+    // respected by other littlefs drivers.
     char name[LFS_NAME_MAX+1];
 };
 
+// Custom attribute structure, used to describe custom attributes
+// committed atomically during file writes.
+struct lfs_attr {
+    // 8-bit type of attribute, provided by user and used to
+    // identify the attribute
+    uint8_t type;
 
-/// littlefs data structures ///
-typedef struct lfs_entry {
-    lfs_off_t off;
+    // Pointer to buffer containing the attribute
+    void *buffer;
+
+    // Size of attribute in bytes, limited to LFS_ATTR_MAX
+    lfs_size_t size;
+};
+
+// Optional configuration provided during lfs_file_opencfg
+struct lfs_file_config {
+    // Optional statically allocated file buffer. Must be cache_size.
+    // By default lfs_malloc is used to allocate this buffer.
+    void *buffer;
+
+    // Optional list of custom attributes related to the file. If the file
+    // is opened with read access, these attributes will be read from disk
+    // during the open call. If the file is opened with write access, the
+    // attributes will be written to disk every file sync or close. This
+    // write occurs atomically with update to the file's contents.
+    //
+    // Custom attributes are uniquely identified by an 8-bit type and limited
+    // to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller
+    // than the buffer, it will be padded with zeros. If the stored attribute
+    // is larger, then it will be silently truncated. If the attribute is not
+    // found, it will be created implicitly.
+    struct lfs_attr *attrs;
+
+    // Number of custom attributes in the list
+    lfs_size_t attr_count;
+};
 
-    struct lfs_disk_entry {
-        uint8_t type;
-        uint8_t elen;
-        uint8_t alen;
-        uint8_t nlen;
-        union {
-            struct {
-                lfs_block_t head;
-                lfs_size_t size;
-            } file;
-            lfs_block_t dir[2];
-        } u;
-    } d;
-} lfs_entry_t;
 
+/// internal littlefs data structures ///
 typedef struct lfs_cache {
     lfs_block_t block;
     lfs_off_t off;
+    lfs_size_t size;
     uint8_t *buffer;
 } lfs_cache_t;
 
+typedef struct lfs_mdir {
+    lfs_block_t pair[2];
+    uint32_t rev;
+    lfs_off_t off;
+    uint32_t etag;
+    uint16_t count;
+    bool erased;
+    bool split;
+    lfs_block_t tail[2];
+} lfs_mdir_t;
+
+// littlefs directory type
+typedef struct lfs_dir {
+    struct lfs_dir *next;
+    uint16_t id;
+    uint8_t type;
+    lfs_mdir_t m;
+
+    lfs_off_t pos;
+    lfs_block_t head[2];
+} lfs_dir_t;
+
+// littlefs file type
 typedef struct lfs_file {
     struct lfs_file *next;
-    lfs_block_t pair[2];
-    lfs_off_t poff;
+    uint16_t id;
+    uint8_t type;
+    lfs_mdir_t m;
 
-    lfs_block_t head;
-    lfs_size_t size;
+    struct lfs_ctz {
+        lfs_block_t head;
+        lfs_size_t size;
+    } ctz;
 
-    const struct lfs_file_config *cfg;
     uint32_t flags;
     lfs_off_t pos;
     lfs_block_t block;
     lfs_off_t off;
     lfs_cache_t cache;
-} lfs_file_t;
-
-typedef struct lfs_dir {
-    struct lfs_dir *next;
-    lfs_block_t pair[2];
-    lfs_off_t off;
-
-    lfs_block_t head[2];
-    lfs_off_t pos;
 
-    struct lfs_disk_dir {
-        uint32_t rev;
-        lfs_size_t size;
-        lfs_block_t tail[2];
-    } d;
-} lfs_dir_t;
+    const struct lfs_file_config *cfg;
+} lfs_file_t;
 
 typedef struct lfs_superblock {
-    lfs_off_t off;
-
-    struct lfs_disk_superblock {
-        uint8_t type;
-        uint8_t elen;
-        uint8_t alen;
-        uint8_t nlen;
-        lfs_block_t root[2];
-        uint32_t block_size;
-        uint32_t block_count;
-        uint32_t version;
-        char magic[8];
-    } d;
+    uint32_t version;
+    lfs_size_t block_size;
+    lfs_size_t block_count;
+    lfs_size_t name_max;
+    lfs_size_t file_max;
+    lfs_size_t attr_max;
 } lfs_superblock_t;
 
-typedef struct lfs_free {
-    lfs_block_t off;
-    lfs_block_t size;
-    lfs_block_t i;
-    lfs_block_t ack;
-    uint32_t *buffer;
-} lfs_free_t;
-
-// The littlefs type
+// The littlefs filesystem type
 typedef struct lfs {
-    const struct lfs_config *cfg;
+    lfs_cache_t rcache;
+    lfs_cache_t pcache;
 
     lfs_block_t root[2];
-    lfs_file_t *files;
-    lfs_dir_t *dirs;
+    struct lfs_mlist {
+        struct lfs_mlist *next;
+        uint16_t id;
+        uint8_t type;
+        lfs_mdir_t m;
+    } *mlist;
+    uint32_t seed;
+
+    struct lfs_gstate {
+        uint32_t tag;
+        lfs_block_t pair[2];
+    } gstate, gpending, gdelta;
+
+    struct lfs_free {
+        lfs_block_t off;
+        lfs_block_t size;
+        lfs_block_t i;
+        lfs_block_t ack;
+        uint32_t *buffer;
+    } free;
 
-    lfs_cache_t rcache;
-    lfs_cache_t pcache;
+    const struct lfs_config *cfg;
+    lfs_size_t name_max;
+    lfs_size_t file_max;
+    lfs_size_t attr_max;
 
-    lfs_free_t free;
-    bool deorphaned;
-    bool moving;
+#ifdef LFS_MIGRATE
+    struct lfs1 *lfs1;
+#endif
 } lfs_t;
 
 
@@ -339,6 +437,38 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath);
 // Returns a negative error code on failure.
 int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info);
 
+// Get a custom attribute
+//
+// Custom attributes are uniquely identified by an 8-bit type and limited
+// to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller than
+// the buffer, it will be padded with zeros. If the stored attribute is larger,
+// then it will be silently truncated. If no attribute is found, the error
+// LFS_ERR_NOATTR is returned and the buffer is filled with zeros.
+//
+// Returns the size of the attribute, or a negative error code on failure.
+// Note, the returned size is the size of the attribute on disk, irrespective
+// of the size of the buffer. This can be used to dynamically allocate a buffer
+// or check for existance.
+lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path,
+        uint8_t type, void *buffer, lfs_size_t size);
+
+// Set custom attributes
+//
+// Custom attributes are uniquely identified by an 8-bit type and limited
+// to LFS_ATTR_MAX bytes. If an attribute is not found, it will be
+// implicitly created.
+//
+// Returns a negative error code on failure.
+int lfs_setattr(lfs_t *lfs, const char *path,
+        uint8_t type, const void *buffer, lfs_size_t size);
+
+// Removes a custom attribute
+//
+// If an attribute is not found, nothing happens.
+//
+// Returns a negative error code on failure.
+int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type);
+
 
 /// File operations ///
 
@@ -448,7 +578,8 @@ int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir);
 // Read an entry in the directory
 //
 // Fills out the info structure, based on the specified file or directory.
-// Returns a negative error code on failure.
+// Returns a positive value on success, 0 at the end of directory,
+// or a negative error code on failure.
 int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info);
 
 // Change the position of the directory
@@ -473,7 +604,15 @@ lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir);
 int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir);
 
 
-/// Miscellaneous littlefs specific operations ///
+/// Filesystem-level filesystem operations
+
+// Finds the current size of the filesystem
+//
+// Note: Result is best effort. If files share COW structures, the returned
+// size may be larger than the filesystem actually is.
+//
+// Returns the number of allocated blocks, or a negative error code on failure.
+lfs_ssize_t lfs_fs_size(lfs_t *lfs);
 
 // Traverse through all blocks in use by the filesystem
 //
@@ -482,16 +621,22 @@ int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir);
 // blocks are in use or how much of the storage is available.
 //
 // Returns a negative error code on failure.
-int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data);
+int lfs_fs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data);
 
-// Prunes any recoverable errors that may have occured in the filesystem
+#ifdef LFS_MIGRATE
+// Attempts to migrate a previous version of littlefs
 //
-// Not needed to be called by user unless an operation is interrupted
-// but the filesystem is still mounted. This is already called on first
-// allocation.
+// Behaves similarly to the lfs_format function. Attempts to mount
+// the previous version of littlefs and update the filesystem so it can be
+// mounted with the current version of littlefs.
+//
+// Requires a littlefs object and config struct. This clobbers the littlefs
+// object, and does not leave the filesystem mounted. The config struct must
+// be zeroed for defaults and backwards compatibility.
 //
 // Returns a negative error code on failure.
-int lfs_deorphan(lfs_t *lfs);
+int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg);
+#endif
 
 
 #ifdef __cplusplus

+ 5 - 3
lfs_util.c

@@ -11,7 +11,7 @@
 
 
 // Software CRC implementation with small lookup table
-void lfs_crc(uint32_t *restrict crc, const void *buffer, size_t size) {
+uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size) {
     static const uint32_t rtable[16] = {
         0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
         0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
@@ -22,9 +22,11 @@ void lfs_crc(uint32_t *restrict crc, const void *buffer, size_t size) {
     const uint8_t *data = buffer;
 
     for (size_t i = 0; i < size; i++) {
-        *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 0)) & 0xf];
-        *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 4)) & 0xf];
+        crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 0)) & 0xf];
+        crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 4)) & 0xf];
     }
+
+    return crc;
 }
 
 

+ 39 - 5
lfs_util.h

@@ -11,8 +11,8 @@
 // LFS_CONFIG as a header file to include (-DLFS_CONFIG=lfs_config.h).
 //
 // If LFS_CONFIG is used, none of the default utils will be emitted and must be
-// provided by the config file. To start I would suggest copying lfs_util.h and
-// modifying as needed.
+// provided by the config file. To start, I would suggest copying lfs_util.h
+// and modifying as needed.
 #ifdef LFS_CONFIG
 #define LFS_STRINGIZE(x) LFS_STRINGIZE2(x)
 #define LFS_STRINGIZE2(x) #x
@@ -23,6 +23,7 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include <string.h>
+#include <inttypes.h>
 
 #ifndef LFS_NO_MALLOC
 #include <stdlib.h>
@@ -87,6 +88,15 @@ static inline uint32_t lfs_min(uint32_t a, uint32_t b) {
     return (a < b) ? a : b;
 }
 
+// Align to nearest multiple of a size
+static inline uint32_t lfs_aligndown(uint32_t a, uint32_t alignment) {
+    return a - (a % alignment);
+}
+
+static inline uint32_t lfs_alignup(uint32_t a, uint32_t alignment) {
+    return lfs_aligndown(a + alignment-1, alignment);
+}
+
 // 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))
@@ -130,7 +140,7 @@ static inline int lfs_scmp(uint32_t a, uint32_t b) {
     return (int)(unsigned)(a - b);
 }
 
-// Convert from 32-bit little-endian to native order
+// Convert between 32-bit little-endian and native order
 static inline uint32_t lfs_fromle32(uint32_t a) {
 #if !defined(LFS_NO_INTRINSICS) && ( \
     (defined(  BYTE_ORDER  ) &&   BYTE_ORDER   ==   ORDER_LITTLE_ENDIAN  ) || \
@@ -150,15 +160,39 @@ static inline uint32_t lfs_fromle32(uint32_t a) {
 #endif
 }
 
-// Convert to 32-bit little-endian from native order
 static inline uint32_t lfs_tole32(uint32_t a) {
     return lfs_fromle32(a);
 }
 
+// Convert between 32-bit big-endian and native order
+static inline uint32_t lfs_frombe32(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 __builtin_bswap32(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 a;
+#else
+    return (((uint8_t*)&a)[0] << 24) |
+           (((uint8_t*)&a)[1] << 16) |
+           (((uint8_t*)&a)[2] <<  8) |
+           (((uint8_t*)&a)[3] <<  0);
+#endif
+}
+
+static inline uint32_t lfs_tobe32(uint32_t a) {
+    return lfs_frombe32(a);
+}
+
 // Calculate CRC-32 with polynomial = 0x04c11db7
-void lfs_crc(uint32_t *crc, const void *buffer, size_t size);
+uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size);
 
 // Allocate memory, only used if buffers are not provided to littlefs
+// Note, memory must be 64-bit aligned
 static inline void *lfs_malloc(size_t size) {
 #ifndef LFS_NO_MALLOC
     return malloc(size);

+ 61 - 0
scripts/prefix.py

@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+
+# This script replaces prefixes of files, and symbols in that file.
+# Useful for creating different versions of the codebase that don't
+# conflict at compile time.
+#
+# example:
+# $ ./scripts/prefix.py lfs2
+
+import os
+import os.path
+import re
+import glob
+import itertools
+import tempfile
+import shutil
+import subprocess
+
+DEFAULT_PREFIX = "lfs"
+
+def subn(from_prefix, to_prefix, name):
+    name, count1 = re.subn('\\b'+from_prefix, to_prefix, name)
+    name, count2 = re.subn('\\b'+from_prefix.upper(), to_prefix.upper(), name)
+    name, count3 = re.subn('\\B-D'+from_prefix.upper(),
+            '-D'+to_prefix.upper(), name)
+    return name, count1+count2+count3
+
+def main(from_prefix, to_prefix=None, files=None):
+    if not to_prefix:
+        from_prefix, to_prefix = DEFAULT_PREFIX, from_prefix
+
+    if not files:
+        files = subprocess.check_output([
+                'git', 'ls-tree', '-r', '--name-only', 'HEAD']).split()
+
+    for oldname in files:
+        # Rename any matching file names
+        newname, namecount = subn(from_prefix, to_prefix, oldname)
+        if namecount:
+            subprocess.check_call(['git', 'mv', oldname, newname])
+
+        # Rename any prefixes in file
+        count = 0
+        with open(newname+'~', 'w') as tempf:
+            with open(newname) as newf:
+                for line in newf:
+                    line, n = subn(from_prefix, to_prefix, line)
+                    count += n
+                    tempf.write(line)
+        shutil.copystat(newname, newname+'~')
+        os.rename(newname+'~', newname)
+        subprocess.check_call(['git', 'add', newname])
+
+        # Summary
+        print '%s: %d replacements' % (
+                '%s -> %s' % (oldname, newname) if namecount else oldname,
+                count)
+
+if __name__ == "__main__":
+    import sys
+    sys.exit(main(*sys.argv[1:]))

+ 44 - 0
tests/corrupt.py

@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+import struct
+import sys
+import os
+import argparse
+
+def corrupt(block):
+    with open(block, 'r+b') as file:
+        # skip rev
+        file.read(4)
+
+        # go to last commit
+        tag = 0xffffffff
+        while True:
+            try:
+                ntag, = struct.unpack('>I', file.read(4))
+            except struct.error:
+                break
+
+            tag ^= ntag
+            size = (tag & 0x3ff) if (tag & 0x3ff) != 0x3ff else 0
+            file.seek(size, os.SEEK_CUR)
+
+        # lob off last 3 bytes
+        file.seek(-(size + 3), os.SEEK_CUR)
+        file.truncate()
+
+def main(args):
+    if args.n or not args.blocks:
+        with open('blocks/.history', 'rb') as file:
+            for i in range(int(args.n or 1)):
+                last, = struct.unpack('<I', file.read(4))
+                args.blocks.append('blocks/%x' % last)
+
+    for block in args.blocks:
+        print 'corrupting %s' % block
+        corrupt(block)
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-n')
+    parser.add_argument('blocks', nargs='*')
+    main(parser.parse_args())

+ 112 - 0
tests/debug.py

@@ -0,0 +1,112 @@
+#!/usr/bin/env python2
+
+import struct
+import binascii
+
+TYPES = {
+    (0x700, 0x400): 'splice',
+    (0x7ff, 0x401): 'create',
+    (0x7ff, 0x4ff): 'delete',
+    (0x700, 0x000): 'name',
+    (0x7ff, 0x001): 'name reg',
+    (0x7ff, 0x002): 'name dir',
+    (0x7ff, 0x0ff): 'name superblock',
+    (0x700, 0x200): 'struct',
+    (0x7ff, 0x200): 'struct dir',
+    (0x7ff, 0x202): 'struct ctz',
+    (0x7ff, 0x201): 'struct inline',
+    (0x700, 0x300): 'userattr',
+    (0x700, 0x600): 'tail',
+    (0x7ff, 0x600): 'tail soft',
+    (0x7ff, 0x601): 'tail hard',
+    (0x700, 0x700): 'gstate',
+    (0x7ff, 0x7ff): 'gstate move',
+    (0x700, 0x500): 'crc',
+}
+
+def typeof(type):
+    for prefix in range(12):
+        mask = 0x7ff & ~((1 << prefix)-1)
+        if (mask, type & mask) in TYPES:
+            return TYPES[mask, type & mask] + (
+                ' %0*x' % (prefix/4, type & ((1 << prefix)-1))
+                if prefix else '')
+    else:
+        return '%02x' % type
+
+def main(*blocks):
+    # find most recent block
+    file = None
+    rev = None
+    crc = None
+    versions = []
+
+    for block in blocks:
+        try:
+            nfile = open(block, 'rb')
+            ndata = nfile.read(4)
+            ncrc = binascii.crc32(ndata)
+            nrev, = struct.unpack('<I', ndata)
+
+            assert rev != nrev
+            if not file or ((rev - nrev) & 0x80000000):
+                file = nfile
+                rev = nrev
+                crc = ncrc
+
+            versions.append((nrev, '%s (rev %d)' % (block, nrev)))
+        except (IOError, struct.error):
+            pass
+
+    if not file:
+        print 'Bad metadata pair {%s}' % ', '.join(blocks)
+        return 1
+
+    print "--- %s ---" % ', '.join(v for _,v in sorted(versions, reverse=True))
+
+    # go through each tag, print useful information
+    print "%-4s  %-8s  %-14s  %3s %4s  %s" % (
+        'off', 'tag', 'type', 'id', 'len', 'dump')
+
+    tag = 0xffffffff
+    off = 4
+    while True:
+        try:
+            data = file.read(4)
+            crc = binascii.crc32(data, crc)
+            ntag, = struct.unpack('>I', data)
+        except struct.error:
+            break
+
+        tag ^= ntag
+        off += 4
+
+        type = (tag & 0x7ff00000) >> 20
+        id   = (tag & 0x000ffc00) >> 10
+        size = (tag & 0x000003ff) >> 0
+        iscrc = (type & 0x700) == 0x500
+
+        data = file.read(size if size != 0x3ff else 0)
+        if iscrc:
+            crc = binascii.crc32(data[:4], crc)
+        else:
+            crc = binascii.crc32(data, crc)
+
+        print '%04x: %08x  %-15s %3s %4s  %-23s  %-8s' % (
+            off, tag,
+            typeof(type) + (' bad!' if iscrc and ~crc else ''),
+            id if id != 0x3ff else '.',
+            size if size != 0x3ff else 'x',
+            ' '.join('%02x' % ord(c) for c in data[:8]),
+            ''.join(c if c >= ' ' and c <= '~' else '.' for c in data[:8]))
+
+        off += size if size != 0x3ff else 0
+        if iscrc:
+            crc = 0
+            tag ^= (type & 1) << 31
+
+    return 0
+
+if __name__ == "__main__":
+    import sys
+    sys.exit(main(*sys.argv[1:]))

+ 2 - 2
tests/stats.py

@@ -7,7 +7,7 @@ import os
 import re
 
 def main():
-    with open('blocks/config') as file:
+    with open('blocks/.config') as file:
         s = struct.unpack('<LLLL', file.read())
         print 'read_size: %d' % s[0]
         print 'prog_size: %d' % s[1]
@@ -18,7 +18,7 @@ def main():
         os.path.getsize(os.path.join('blocks', f))
         for f in os.listdir('blocks') if re.match('\d+', f))
 
-    with open('blocks/stats') as file:
+    with open('blocks/.stats') as file:
         s = struct.unpack('<QQQ', file.read())
         print 'read_count: %d' % s[0]
         print 'prog_count: %d' % s[1]

+ 18 - 8
tests/template.fmt

@@ -66,7 +66,7 @@ uintmax_t test;
 #endif
 
 #ifndef LFS_PROG_SIZE
-#define LFS_PROG_SIZE 16
+#define LFS_PROG_SIZE LFS_READ_SIZE
 #endif
 
 #ifndef LFS_BLOCK_SIZE
@@ -77,8 +77,16 @@ uintmax_t test;
 #define LFS_BLOCK_COUNT 1024
 #endif
 
-#ifndef LFS_LOOKAHEAD
-#define LFS_LOOKAHEAD 128
+#ifndef LFS_BLOCK_CYCLES
+#define LFS_BLOCK_CYCLES 1024
+#endif
+
+#ifndef LFS_CACHE_SIZE
+#define LFS_CACHE_SIZE 64
+#endif
+
+#ifndef LFS_LOOKAHEAD_SIZE
+#define LFS_LOOKAHEAD_SIZE 16
 #endif
 
 const struct lfs_config cfg = {{
@@ -88,11 +96,13 @@ const struct lfs_config cfg = {{
     .erase = &lfs_emubd_erase,
     .sync  = &lfs_emubd_sync,
 
-    .read_size   = LFS_READ_SIZE,
-    .prog_size   = LFS_PROG_SIZE,
-    .block_size  = LFS_BLOCK_SIZE,
-    .block_count = LFS_BLOCK_COUNT,
-    .lookahead   = LFS_LOOKAHEAD,
+    .read_size      = LFS_READ_SIZE,
+    .prog_size      = LFS_PROG_SIZE,
+    .block_size     = LFS_BLOCK_SIZE,
+    .block_count    = LFS_BLOCK_COUNT,
+    .block_cycles   = LFS_BLOCK_CYCLES,
+    .cache_size     = LFS_CACHE_SIZE,
+    .lookahead_size = LFS_LOOKAHEAD_SIZE,
 }};
 
 

+ 1 - 1
tests/test.py

@@ -10,7 +10,7 @@ def generate(test):
         template = file.read()
 
     lines = []
-    for line in re.split('(?<=[;{}])\n', test.read()):
+    for line in re.split('(?<=(?:.;| [{}]))\n', test.read()):
         match = re.match('(?: *\n)*( *)(.*)=>(.*);', line, re.DOTALL | re.MULTILINE)
         if match:
             tab, test, expect = match.groups()

+ 98 - 41
tests/test_alloc.sh

@@ -194,76 +194,125 @@ tests/test.py << TEST
     lfs_file_read(&lfs, &file[0], buffer, size) => size;
     memcmp(buffer, "exhaustion", size) => 0;
     lfs_file_close(&lfs, &file[0]) => 0;
+    lfs_remove(&lfs, "exhaustion") => 0;
     lfs_unmount(&lfs) => 0;
 TEST
 
 echo "--- Dir exhaustion test ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
-    lfs_remove(&lfs, "exhaustion") => 0;
 
-    lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
+    // find out max file size
+    lfs_mkdir(&lfs, "exhaustiondir") => 0;
     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_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
+    int count = 0;
+    int err;
+    while (true) {
+        err = lfs_file_write(&lfs, &file[0], buffer, size);
+        if (err < 0) {
+            break;
+        }
+
+        count += 1;
+    }
+    err => LFS_ERR_NOSPC;
+    lfs_file_close(&lfs, &file[0]) => 0;
+
+    lfs_remove(&lfs, "exhaustion") => 0;
+    lfs_remove(&lfs, "exhaustiondir") => 0;
+
+    // see if dir fits with max file size
+    lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
+    for (int i = 0; i < count; i++) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
     lfs_file_close(&lfs, &file[0]) => 0;
 
     lfs_mkdir(&lfs, "exhaustiondir") => 0;
     lfs_remove(&lfs, "exhaustiondir") => 0;
+    lfs_remove(&lfs, "exhaustion") => 0;
 
-    lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_APPEND);
-    size = strlen("blahblahblahblah");
-    memcpy(buffer, "blahblahblahblah", size);
-    for (lfs_size_t i = 0;
-            i < (cfg.block_size-8);
-            i += size) {
+    // see if dir fits with > max file size
+    lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
+    for (int i = 0; i < count+1; i++) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
     lfs_file_close(&lfs, &file[0]) => 0;
 
     lfs_mkdir(&lfs, "exhaustiondir") => LFS_ERR_NOSPC;
+
+    lfs_remove(&lfs, "exhaustion") => 0;
     lfs_unmount(&lfs) => 0;
 TEST
 
 echo "--- Chained dir exhaustion test ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
-    lfs_remove(&lfs, "exhaustion") => 0;
 
-    lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
+    // find out max file size
+    lfs_mkdir(&lfs, "exhaustiondir") => 0;
+    for (int i = 0; i < 10; i++) {
+        sprintf((char*)buffer, "dirwithanexhaustivelylongnameforpadding%d", i);
+        lfs_mkdir(&lfs, (char*)buffer) => 0;
+    }
     size = strlen("blahblahblahblah");
     memcpy(buffer, "blahblahblahblah", 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_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
+    int count = 0;
+    int err;
+    while (true) {
+        err = lfs_file_write(&lfs, &file[0], buffer, size);
+        if (err < 0) {
+            break;
+        }
+
+        count += 1;
     }
+    err => LFS_ERR_NOSPC;
     lfs_file_close(&lfs, &file[0]) => 0;
 
-    for (int i = 0; i < 9; i++) {
+    lfs_remove(&lfs, "exhaustion") => 0;
+    lfs_remove(&lfs, "exhaustiondir") => 0;
+    for (int i = 0; i < 10; i++) {
+        sprintf((char*)buffer, "dirwithanexhaustivelylongnameforpadding%d", i);
+        lfs_remove(&lfs, (char*)buffer) => 0;
+    }
+
+    // see that chained dir fails
+    lfs_file_open(&lfs, &file[0], "exhaustion", LFS_O_WRONLY | LFS_O_CREAT);
+    for (int i = 0; i < count+1; i++) {
+        lfs_file_write(&lfs, &file[0], buffer, size) => size;
+    }
+    lfs_file_sync(&lfs, &file[0]) => 0;
+
+    for (int i = 0; i < 10; i++) {
         sprintf((char*)buffer, "dirwithanexhaustivelylongnameforpadding%d", i);
         lfs_mkdir(&lfs, (char*)buffer) => 0;
     }
 
     lfs_mkdir(&lfs, "exhaustiondir") => LFS_ERR_NOSPC;
 
-    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 < (cfg.block_count-26)*(cfg.block_size-8);
-            i += size) {
-        lfs_file_write(&lfs, &file[0], buffer, size) => size;
+    // shorten file to try a second chained dir
+    while (true) {
+        err = lfs_mkdir(&lfs, "exhaustiondir");
+        if (err != LFS_ERR_NOSPC) {
+            break;
+        }
+
+        lfs_ssize_t filesize = lfs_file_size(&lfs, &file[0]);
+        filesize > 0 => true;
+
+        lfs_file_truncate(&lfs, &file[0], filesize - size) => 0;
+        lfs_file_sync(&lfs, &file[0]) => 0;
     }
-    lfs_file_close(&lfs, &file[0]) => 0;
+    err => 0;
 
-    lfs_mkdir(&lfs, "exhaustiondir") => 0;
     lfs_mkdir(&lfs, "exhaustiondir2") => LFS_ERR_NOSPC;
+
+    lfs_file_close(&lfs, &file[0]) => 0;
+    lfs_unmount(&lfs) => 0;
 TEST
 
 echo "--- Split dir test ---"
@@ -274,28 +323,38 @@ TEST
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
 
-    // create one block whole for half a directory
+    // create one block hole 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;
+    for (lfs_size_t i = 0; i < cfg.block_size; i += 2) {
+        memcpy(&buffer[i], "hi", 2);
+    }
+    lfs_file_write(&lfs, &file[0], buffer, cfg.block_size) => cfg.block_size;
     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 < (cfg.block_count-4)*(cfg.block_size-8);
             i += size) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
     lfs_file_close(&lfs, &file[0]) => 0;
 
+    // remount to force reset of lookahead
+    lfs_unmount(&lfs) => 0;
+    lfs_mount(&lfs, &cfg) => 0;
+
     // open hole
     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;
+    for (lfs_size_t i = 0; i < cfg.block_size; i += 2) {
+        memcpy(&buffer[i], "hi", 2);
+    }
+    lfs_file_write(&lfs, &file[0], buffer, 2*cfg.block_size) => LFS_ERR_NOSPC;
     lfs_file_close(&lfs, &file[0]) => 0;
 
     lfs_unmount(&lfs) => 0;
@@ -314,7 +373,7 @@ tests/test.py << TEST
     size = strlen("blahblahblahblah");
     memcpy(buffer, "blahblahblahblah", size);
     for (lfs_size_t i = 0;
-            i < ((cfg.block_count-4)/2)*(cfg.block_size-8);
+            i < ((cfg.block_count-2)/2)*(cfg.block_size-8);
             i += size) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
@@ -325,7 +384,7 @@ tests/test.py << TEST
     size = strlen("blahblahblahblah");
     memcpy(buffer, "blahblahblahblah", size);
     for (lfs_size_t i = 0;
-            i < ((cfg.block_count-4+1)/2)*(cfg.block_size-8);
+            i < ((cfg.block_count-2+1)/2)*(cfg.block_size-8);
             i += size) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
@@ -333,7 +392,6 @@ tests/test.py << TEST
 
     // remount to force reset of lookahead
     lfs_unmount(&lfs) => 0;
-
     lfs_mount(&lfs, &cfg) => 0;
 
     // rewrite one file
@@ -343,7 +401,7 @@ tests/test.py << TEST
     size = strlen("blahblahblahblah");
     memcpy(buffer, "blahblahblahblah", size);
     for (lfs_size_t i = 0;
-            i < ((cfg.block_count-4)/2)*(cfg.block_size-8);
+            i < ((cfg.block_count-2)/2)*(cfg.block_size-8);
             i += size) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
@@ -357,7 +415,7 @@ tests/test.py << TEST
     size = strlen("blahblahblahblah");
     memcpy(buffer, "blahblahblahblah", size);
     for (lfs_size_t i = 0;
-            i < ((cfg.block_count-4+1)/2)*(cfg.block_size-8);
+            i < ((cfg.block_count-2+1)/2)*(cfg.block_size-8);
             i += size) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
@@ -377,7 +435,7 @@ tests/test.py << TEST
     size = strlen("blahblahblahblah");
     memcpy(buffer, "blahblahblahblah", size);
     for (lfs_size_t i = 0;
-            i < ((cfg.block_count-4)/2)*(cfg.block_size-8);
+            i < ((cfg.block_count-2)/2)*(cfg.block_size-8);
             i += size) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
@@ -388,7 +446,7 @@ tests/test.py << TEST
     size = strlen("blahblahblahblah");
     memcpy(buffer, "blahblahblahblah", size);
     for (lfs_size_t i = 0;
-            i < ((cfg.block_count-4+1)/2)*(cfg.block_size-8);
+            i < ((cfg.block_count-2+1)/2)*(cfg.block_size-8);
             i += size) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }
@@ -396,7 +454,6 @@ tests/test.py << TEST
 
     // remount to force reset of lookahead
     lfs_unmount(&lfs) => 0;
-
     lfs_mount(&lfs, &cfg) => 0;
 
     // rewrite one file with a hole of one block
@@ -406,7 +463,7 @@ tests/test.py << TEST
     size = strlen("blahblahblahblah");
     memcpy(buffer, "blahblahblahblah", size);
     for (lfs_size_t i = 0;
-            i < ((cfg.block_count-4)/2 - 1)*(cfg.block_size-8);
+            i < ((cfg.block_count-2)/2 - 1)*(cfg.block_size-8);
             i += size) {
         lfs_file_write(&lfs, &file[0], buffer, size) => size;
     }

+ 286 - 0
tests/test_attrs.sh

@@ -0,0 +1,286 @@
+#!/bin/bash
+set -eu
+
+echo "=== Attr tests ==="
+rm -rf blocks
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_mkdir(&lfs, "hello") => 0;
+    lfs_file_open(&lfs, &file[0], "hello/hello",
+            LFS_O_WRONLY | LFS_O_CREAT) => 0;
+    lfs_file_write(&lfs, &file[0], "hello", strlen("hello"))
+            => strlen("hello");
+    lfs_file_close(&lfs, &file[0]);
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Set/get attribute ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_setattr(&lfs, "hello", 'A', "aaaa",   4) => 0;
+    lfs_setattr(&lfs, "hello", 'B', "bbbbbb", 6) => 0;
+    lfs_setattr(&lfs, "hello", 'C', "ccccc",  5) => 0;
+    lfs_getattr(&lfs, "hello", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "hello", 'B', buffer+4,  6) => 6;
+    lfs_getattr(&lfs, "hello", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",   4) => 0;
+    memcmp(buffer+4,  "bbbbbb", 6) => 0;
+    memcmp(buffer+10, "ccccc",  5) => 0;
+
+    lfs_setattr(&lfs, "hello", 'B', "", 0) => 0;
+    lfs_getattr(&lfs, "hello", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "hello", 'B', buffer+4,  6) => 0;
+    lfs_getattr(&lfs, "hello", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",         4) => 0;
+    memcmp(buffer+4,  "\0\0\0\0\0\0", 6) => 0;
+    memcmp(buffer+10, "ccccc",        5) => 0;
+
+    lfs_removeattr(&lfs, "hello", 'B') => 0;
+    lfs_getattr(&lfs, "hello", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "hello", 'B', buffer+4,  6) => LFS_ERR_NOATTR;
+    lfs_getattr(&lfs, "hello", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",         4) => 0;
+    memcmp(buffer+4,  "\0\0\0\0\0\0", 6) => 0;
+    memcmp(buffer+10, "ccccc",        5) => 0;
+
+    lfs_setattr(&lfs, "hello", 'B', "dddddd", 6) => 0;
+    lfs_getattr(&lfs, "hello", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "hello", 'B', buffer+4,  6) => 6;
+    lfs_getattr(&lfs, "hello", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",   4) => 0;
+    memcmp(buffer+4,  "dddddd", 6) => 0;
+    memcmp(buffer+10, "ccccc",  5) => 0;
+
+    lfs_setattr(&lfs, "hello", 'B', "eee", 3) => 0;
+    lfs_getattr(&lfs, "hello", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "hello", 'B', buffer+4,  6) => 3;
+    lfs_getattr(&lfs, "hello", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",      4) => 0;
+    memcmp(buffer+4,  "eee\0\0\0", 6) => 0;
+    memcmp(buffer+10, "ccccc",     5) => 0;
+
+    lfs_setattr(&lfs, "hello", 'A', buffer, LFS_ATTR_MAX+1) => LFS_ERR_NOSPC;
+    lfs_setattr(&lfs, "hello", 'B', "fffffffff", 9) => 0;
+    lfs_getattr(&lfs, "hello", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "hello", 'B', buffer+4,  6) => 9;
+    lfs_getattr(&lfs, "hello", 'C', buffer+10, 5) => 5;
+
+    lfs_unmount(&lfs) => 0;
+TEST
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_getattr(&lfs, "hello", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "hello", 'B', buffer+4,  9) => 9;
+    lfs_getattr(&lfs, "hello", 'C', buffer+13, 5) => 5;
+    memcmp(buffer,    "aaaa",      4) => 0;
+    memcmp(buffer+4,  "fffffffff", 9) => 0;
+    memcmp(buffer+13, "ccccc",     5) => 0;
+
+    lfs_file_open(&lfs, &file[0], "hello/hello", LFS_O_RDONLY) => 0;
+    lfs_file_read(&lfs, &file[0], buffer, sizeof(buffer)) => strlen("hello");
+    memcmp(buffer, "hello", strlen("hello")) => 0;
+    lfs_file_close(&lfs, &file[0]);
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Set/get root attribute ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_setattr(&lfs, "/", 'A', "aaaa",   4) => 0;
+    lfs_setattr(&lfs, "/", 'B', "bbbbbb", 6) => 0;
+    lfs_setattr(&lfs, "/", 'C', "ccccc",  5) => 0;
+    lfs_getattr(&lfs, "/", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "/", 'B', buffer+4,  6) => 6;
+    lfs_getattr(&lfs, "/", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",   4) => 0;
+    memcmp(buffer+4,  "bbbbbb", 6) => 0;
+    memcmp(buffer+10, "ccccc",  5) => 0;
+
+    lfs_setattr(&lfs, "/", 'B', "", 0) => 0;
+    lfs_getattr(&lfs, "/", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "/", 'B', buffer+4,  6) => 0;
+    lfs_getattr(&lfs, "/", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",         4) => 0;
+    memcmp(buffer+4,  "\0\0\0\0\0\0", 6) => 0;
+    memcmp(buffer+10, "ccccc",        5) => 0;
+
+    lfs_removeattr(&lfs, "/", 'B') => 0;
+    lfs_getattr(&lfs, "/", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "/", 'B', buffer+4,  6) => LFS_ERR_NOATTR;
+    lfs_getattr(&lfs, "/", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",         4) => 0;
+    memcmp(buffer+4,  "\0\0\0\0\0\0", 6) => 0;
+    memcmp(buffer+10, "ccccc",        5) => 0;
+
+    lfs_setattr(&lfs, "/", 'B', "dddddd", 6) => 0;
+    lfs_getattr(&lfs, "/", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "/", 'B', buffer+4,  6) => 6;
+    lfs_getattr(&lfs, "/", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",   4) => 0;
+    memcmp(buffer+4,  "dddddd", 6) => 0;
+    memcmp(buffer+10, "ccccc",  5) => 0;
+
+    lfs_setattr(&lfs, "/", 'B', "eee", 3) => 0;
+    lfs_getattr(&lfs, "/", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "/", 'B', buffer+4,  6) => 3;
+    lfs_getattr(&lfs, "/", 'C', buffer+10, 5) => 5;
+    memcmp(buffer,    "aaaa",      4) => 0;
+    memcmp(buffer+4,  "eee\0\0\0", 6) => 0;
+    memcmp(buffer+10, "ccccc",     5) => 0;
+
+    lfs_setattr(&lfs, "/", 'A', buffer, LFS_ATTR_MAX+1) => LFS_ERR_NOSPC;
+    lfs_setattr(&lfs, "/", 'B', "fffffffff", 9) => 0;
+    lfs_getattr(&lfs, "/", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "/", 'B', buffer+4,  6) => 9;
+    lfs_getattr(&lfs, "/", 'C', buffer+10, 5) => 5;
+    lfs_unmount(&lfs) => 0;
+TEST
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_getattr(&lfs, "/", 'A', buffer,    4) => 4;
+    lfs_getattr(&lfs, "/", 'B', buffer+4,  9) => 9;
+    lfs_getattr(&lfs, "/", 'C', buffer+13, 5) => 5;
+    memcmp(buffer,    "aaaa",      4) => 0;
+    memcmp(buffer+4,  "fffffffff", 9) => 0;
+    memcmp(buffer+13, "ccccc",     5) => 0;
+
+    lfs_file_open(&lfs, &file[0], "hello/hello", LFS_O_RDONLY) => 0;
+    lfs_file_read(&lfs, &file[0], buffer, sizeof(buffer)) => strlen("hello");
+    memcmp(buffer, "hello", strlen("hello")) => 0;
+    lfs_file_close(&lfs, &file[0]);
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Set/get file attribute ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    struct lfs_attr attrs1[] = {
+        {'A', buffer,    4},
+        {'B', buffer+4,  6},
+        {'C', buffer+10, 5},
+    };
+    struct lfs_file_config cfg1 = {.attrs=attrs1, .attr_count=3};
+
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_WRONLY, &cfg1) => 0;
+    memcpy(buffer,    "aaaa",   4);
+    memcpy(buffer+4,  "bbbbbb", 6);
+    memcpy(buffer+10, "ccccc",  5);
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memset(buffer, 0, 15);
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_RDONLY, &cfg1) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memcmp(buffer,    "aaaa",   4) => 0;
+    memcmp(buffer+4,  "bbbbbb", 6) => 0;
+    memcmp(buffer+10, "ccccc",  5) => 0;
+
+    attrs1[1].size = 0;
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_WRONLY, &cfg1) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memset(buffer, 0, 15);
+    attrs1[1].size = 6;
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_RDONLY, &cfg1) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memcmp(buffer,    "aaaa",         4) => 0;
+    memcmp(buffer+4,  "\0\0\0\0\0\0", 6) => 0;
+    memcmp(buffer+10, "ccccc",        5) => 0;
+
+    attrs1[1].size = 6;
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_WRONLY, &cfg1) => 0;
+    memcpy(buffer+4,  "dddddd", 6);
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memset(buffer, 0, 15);
+    attrs1[1].size = 6;
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_RDONLY, &cfg1) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memcmp(buffer,    "aaaa",   4) => 0;
+    memcmp(buffer+4,  "dddddd", 6) => 0;
+    memcmp(buffer+10, "ccccc",  5) => 0;
+
+    attrs1[1].size = 3;
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_WRONLY, &cfg1) => 0;
+    memcpy(buffer+4,  "eee", 3);
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memset(buffer, 0, 15);
+    attrs1[1].size = 6;
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_RDONLY, &cfg1) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memcmp(buffer,    "aaaa",      4) => 0;
+    memcmp(buffer+4,  "eee\0\0\0", 6) => 0;
+    memcmp(buffer+10, "ccccc",     5) => 0;
+
+    attrs1[0].size = LFS_ATTR_MAX+1;
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_WRONLY, &cfg1)
+        => LFS_ERR_NOSPC;
+
+    struct lfs_attr attrs2[] = {
+        {'A', buffer,    4},
+        {'B', buffer+4,  9},
+        {'C', buffer+13, 5},
+    };
+    struct lfs_file_config cfg2 = {.attrs=attrs2, .attr_count=3};
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_RDWR, &cfg2) => 0;
+    memcpy(buffer+4,  "fffffffff", 9);
+    lfs_file_close(&lfs, &file[0]) => 0;
+    attrs1[0].size = 4;
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_RDONLY, &cfg1) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+
+    lfs_unmount(&lfs) => 0;
+TEST
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    struct lfs_attr attrs2[] = {
+        {'A', buffer,    4},
+        {'B', buffer+4,  9},
+        {'C', buffer+13, 5},
+    };
+    struct lfs_file_config cfg2 = {.attrs=attrs2, .attr_count=3};
+
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_RDONLY, &cfg2) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    memcmp(buffer,    "aaaa",      4) => 0;
+    memcmp(buffer+4,  "fffffffff", 9) => 0;
+    memcmp(buffer+13, "ccccc",     5) => 0;
+
+    lfs_file_open(&lfs, &file[0], "hello/hello", LFS_O_RDONLY) => 0;
+    lfs_file_read(&lfs, &file[0], buffer, sizeof(buffer)) => strlen("hello");
+    memcmp(buffer, "hello", strlen("hello")) => 0;
+    lfs_file_close(&lfs, &file[0]);
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Deferred file attributes ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    struct lfs_attr attrs1[] = {
+        {'B', "gggg", 4},
+        {'C', "",     0},
+        {'D', "hhhh", 4},
+    };
+    struct lfs_file_config cfg1 = {.attrs=attrs1, .attr_count=3};
+
+    lfs_file_opencfg(&lfs, &file[0], "hello/hello", LFS_O_WRONLY, &cfg1) => 0;
+
+    lfs_getattr(&lfs, "hello/hello", 'B', buffer,    9) => 9;
+    lfs_getattr(&lfs, "hello/hello", 'C', buffer+9,  9) => 5;
+    lfs_getattr(&lfs, "hello/hello", 'D', buffer+18, 9) => LFS_ERR_NOATTR;
+    memcmp(buffer,    "fffffffff",          9) => 0;
+    memcmp(buffer+9,  "ccccc\0\0\0\0",      9) => 0;
+    memcmp(buffer+18, "\0\0\0\0\0\0\0\0\0", 9) => 0;
+
+    lfs_file_sync(&lfs, &file[0]) => 0;
+    lfs_getattr(&lfs, "hello/hello", 'B', buffer,    9) => 4;
+    lfs_getattr(&lfs, "hello/hello", 'C', buffer+9,  9) => 0;
+    lfs_getattr(&lfs, "hello/hello", 'D', buffer+18, 9) => 4;
+    memcmp(buffer,    "gggg\0\0\0\0\0",     9) => 0;
+    memcmp(buffer+9,  "\0\0\0\0\0\0\0\0\0", 9) => 0;
+    memcmp(buffer+18, "hhhh\0\0\0\0\0",     9) => 0;
+
+    lfs_file_close(&lfs, &file[0]) => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Results ---"
+tests/stats.py

+ 7 - 6
tests/test_corrupt.sh

@@ -71,24 +71,25 @@ echo "--- Sanity check ---"
 rm -rf blocks
 lfs_mktree
 lfs_chktree
+BLOCKS="$(ls blocks | grep -vw '[01]')"
 
 echo "--- Block corruption ---"
-for i in {0..33}
+for b in $BLOCKS
 do 
     rm -rf blocks
     mkdir blocks
-    ln -s /dev/zero blocks/$(printf '%x' $i)
+    ln -s /dev/zero blocks/$b
     lfs_mktree
     lfs_chktree
 done
 
 echo "--- Block persistance ---"
-for i in {0..33}
+for b in $BLOCKS
 do 
     rm -rf blocks
     mkdir blocks
     lfs_mktree
-    chmod a-w blocks/$(printf '%x' $i)
+    chmod a-w blocks/$b || true
     lfs_mktree
     lfs_chktree
 done
@@ -96,7 +97,7 @@ done
 echo "--- Big region corruption ---"
 rm -rf blocks
 mkdir blocks
-for i in {2..255}
+for i in {2..512}
 do
     ln -s /dev/zero blocks/$(printf '%x' $i)
 done
@@ -106,7 +107,7 @@ lfs_chktree
 echo "--- Alternating corruption ---"
 rm -rf blocks
 mkdir blocks
-for i in {2..511..2}
+for i in {2..1024..2}
 do
     ln -s /dev/zero blocks/$(printf '%x' $i)
 done

+ 23 - 23
tests/test_dirs.sh

@@ -43,11 +43,11 @@ tests/test.py << TEST
     strcmp(info.name, "..") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "potato") => 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, "potato") => 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;
@@ -85,10 +85,10 @@ tests/test.py << TEST
     strcmp(info.name, "baked") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "sweet") => 0;
+    strcmp(info.name, "fried") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "fried") => 0;
+    strcmp(info.name, "sweet") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_dir_close(&lfs, &dir[0]) => 0;
@@ -100,7 +100,7 @@ tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     lfs_mkdir(&lfs, "cactus") => 0;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "cactus/test%d", i);
+        sprintf((char*)buffer, "cactus/test%03d", i);
         lfs_mkdir(&lfs, (char*)buffer) => 0;
     }
     lfs_unmount(&lfs) => 0;
@@ -115,7 +115,7 @@ tests/test.py << TEST
     strcmp(info.name, "..") => 0;
     info.type => LFS_TYPE_DIR;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "test%d", i);
+        sprintf((char*)buffer, "test%03d", i);
         lfs_dir_read(&lfs, &dir[0], &info) => 1;
         strcmp(info.name, (char*)buffer) => 0;
         info.type => LFS_TYPE_DIR;
@@ -208,10 +208,10 @@ tests/test.py << TEST
     strcmp(info.name, "baked") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "sweet") => 0;
+    strcmp(info.name, "fried") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "fried") => 0;
+    strcmp(info.name, "sweet") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_dir_close(&lfs, &dir[0]) => 0;
@@ -241,10 +241,10 @@ tests/test.py << TEST
     strcmp(info.name, "baked") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "sweet") => 0;
+    strcmp(info.name, "fried") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "fried") => 0;
+    strcmp(info.name, "sweet") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_dir_close(&lfs, &dir[0]) => 0;
@@ -273,10 +273,10 @@ tests/test.py << TEST
     strcmp(info.name, "baked") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "sweet") => 0;
+    strcmp(info.name, "fried") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "fried") => 0;
+    strcmp(info.name, "sweet") => 0;
     info.type => LFS_TYPE_DIR;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_dir_close(&lfs, &dir[0]) => 0;
@@ -330,8 +330,8 @@ echo "--- Multi-block rename ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "cactus/test%d", i);
-        sprintf((char*)wbuffer, "cactus/tedd%d", i);
+        sprintf((char*)buffer, "cactus/test%03d", i);
+        sprintf((char*)wbuffer, "cactus/tedd%03d", i);
         lfs_rename(&lfs, (char*)buffer, (char*)wbuffer) => 0;
     }
     lfs_unmount(&lfs) => 0;
@@ -346,7 +346,7 @@ tests/test.py << TEST
     strcmp(info.name, "..") => 0;
     info.type => LFS_TYPE_DIR;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "tedd%d", i);
+        sprintf((char*)buffer, "tedd%03d", i);
         lfs_dir_read(&lfs, &dir[0], &info) => 1;
         strcmp(info.name, (char*)buffer) => 0;
         info.type => LFS_TYPE_DIR;
@@ -361,7 +361,7 @@ tests/test.py << TEST
     lfs_remove(&lfs, "cactus") => LFS_ERR_NOTEMPTY;
 
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "cactus/tedd%d", i);
+        sprintf((char*)buffer, "cactus/tedd%03d", i);
         lfs_remove(&lfs, (char*)buffer) => 0;
     }
 
@@ -390,7 +390,7 @@ tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     lfs_mkdir(&lfs, "prickly-pear") => 0;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "prickly-pear/test%d", i);
+        sprintf((char*)buffer, "prickly-pear/test%03d", i);
         lfs_file_open(&lfs, &file[0], (char*)buffer,
                 LFS_O_WRONLY | LFS_O_CREAT) => 0;
         size = 6;
@@ -410,7 +410,7 @@ tests/test.py << TEST
     strcmp(info.name, "..") => 0;
     info.type => LFS_TYPE_DIR;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "test%d", i);
+        sprintf((char*)buffer, "test%03d", i);
         lfs_dir_read(&lfs, &dir[0], &info) => 1;
         strcmp(info.name, (char*)buffer) => 0;
         info.type => LFS_TYPE_REG;
@@ -424,8 +424,8 @@ echo "--- Multi-block rename with files ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "prickly-pear/test%d", i);
-        sprintf((char*)wbuffer, "prickly-pear/tedd%d", i);
+        sprintf((char*)buffer, "prickly-pear/test%03d", i);
+        sprintf((char*)wbuffer, "prickly-pear/tedd%03d", i);
         lfs_rename(&lfs, (char*)buffer, (char*)wbuffer) => 0;
     }
     lfs_unmount(&lfs) => 0;
@@ -440,7 +440,7 @@ tests/test.py << TEST
     strcmp(info.name, "..") => 0;
     info.type => LFS_TYPE_DIR;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "tedd%d", i);
+        sprintf((char*)buffer, "tedd%03d", i);
         lfs_dir_read(&lfs, &dir[0], &info) => 1;
         strcmp(info.name, (char*)buffer) => 0;
         info.type => LFS_TYPE_REG;
@@ -456,7 +456,7 @@ tests/test.py << TEST
     lfs_remove(&lfs, "prickly-pear") => LFS_ERR_NOTEMPTY;
 
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "prickly-pear/tedd%d", i);
+        sprintf((char*)buffer, "prickly-pear/tedd%03d", i);
         lfs_remove(&lfs, (char*)buffer) => 0;
     }
 

+ 221 - 0
tests/test_entries.sh

@@ -0,0 +1,221 @@
+#!/bin/bash
+set -eu
+
+# Note: These tests are intended for 512 byte inline size at different
+# inline sizes they should still pass, but won't be testing anything
+
+echo "=== Entry tests ==="
+rm -rf blocks
+function read_file {
+cat << TEST
+
+    size = $2;
+    lfs_file_open(&lfs, &file[0], "$1", LFS_O_RDONLY) => 0;
+    lfs_file_read(&lfs, &file[0], rbuffer, size) => size;
+    memcmp(rbuffer, wbuffer, size) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+TEST
+}
+
+function write_file {
+cat << TEST
+
+    size = $2;
+    lfs_file_open(&lfs, &file[0], "$1",
+            LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
+    memset(wbuffer, 'c', size);
+    lfs_file_write(&lfs, &file[0], wbuffer, size) => size;
+    lfs_file_close(&lfs, &file[0]) => 0;
+TEST
+}
+
+echo "--- Entry grow test ---"
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    $(write_file "hi0" 20)
+    $(write_file "hi1" 20)
+    $(write_file "hi2" 20)
+    $(write_file "hi3" 20)
+
+    $(read_file "hi1" 20)
+    $(write_file "hi1" 200)
+
+    $(read_file "hi0" 20)
+    $(read_file "hi1" 200)
+    $(read_file "hi2" 20)
+    $(read_file "hi3" 20)
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Entry shrink test ---"
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    $(write_file "hi0" 20)
+    $(write_file "hi1" 200)
+    $(write_file "hi2" 20)
+    $(write_file "hi3" 20)
+
+    $(read_file "hi1" 200)
+    $(write_file "hi1" 20)
+
+    $(read_file "hi0" 20)
+    $(read_file "hi1" 20)
+    $(read_file "hi2" 20)
+    $(read_file "hi3" 20)
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Entry spill test ---"
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    $(write_file "hi0" 200)
+    $(write_file "hi1" 200)
+    $(write_file "hi2" 200)
+    $(write_file "hi3" 200)
+
+    $(read_file "hi0" 200)
+    $(read_file "hi1" 200)
+    $(read_file "hi2" 200)
+    $(read_file "hi3" 200)
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Entry push spill test ---"
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    $(write_file "hi0" 200)
+    $(write_file "hi1" 20)
+    $(write_file "hi2" 200)
+    $(write_file "hi3" 200)
+
+    $(read_file "hi1" 20)
+    $(write_file "hi1" 200)
+
+    $(read_file "hi0" 200)
+    $(read_file "hi1" 200)
+    $(read_file "hi2" 200)
+    $(read_file "hi3" 200)
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Entry push spill two test ---"
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    $(write_file "hi0" 200)
+    $(write_file "hi1" 20)
+    $(write_file "hi2" 200)
+    $(write_file "hi3" 200)
+    $(write_file "hi4" 200)
+
+    $(read_file "hi1" 20)
+    $(write_file "hi1" 200)
+
+    $(read_file "hi0" 200)
+    $(read_file "hi1" 200)
+    $(read_file "hi2" 200)
+    $(read_file "hi3" 200)
+    $(read_file "hi4" 200)
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Entry drop test ---"
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    $(write_file "hi0" 200)
+    $(write_file "hi1" 200)
+    $(write_file "hi2" 200)
+    $(write_file "hi3" 200)
+
+    lfs_remove(&lfs, "hi1") => 0;
+    lfs_stat(&lfs, "hi1", &info) => LFS_ERR_NOENT;
+    $(read_file "hi0" 200)
+    $(read_file "hi2" 200)
+    $(read_file "hi3" 200)
+
+    lfs_remove(&lfs, "hi2") => 0;
+    lfs_stat(&lfs, "hi2", &info) => LFS_ERR_NOENT;
+    $(read_file "hi0" 200)
+    $(read_file "hi3" 200)
+
+    lfs_remove(&lfs, "hi3") => 0;
+    lfs_stat(&lfs, "hi3", &info) => LFS_ERR_NOENT;
+    $(read_file "hi0" 200)
+
+    lfs_remove(&lfs, "hi0") => 0;
+    lfs_stat(&lfs, "hi0", &info) => LFS_ERR_NOENT;
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Create too big ---"
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    memset(buffer, 'm', 200);
+    buffer[200] = '\0';
+
+    size = 400;
+    lfs_file_open(&lfs, &file[0], (char*)buffer,
+            LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
+    memset(wbuffer, 'c', size);
+    lfs_file_write(&lfs, &file[0], wbuffer, size) => size;
+    lfs_file_close(&lfs, &file[0]) => 0;
+
+    size = 400;
+    lfs_file_open(&lfs, &file[0], (char*)buffer, LFS_O_RDONLY) => 0;
+    lfs_file_read(&lfs, &file[0], rbuffer, size) => size;
+    memcmp(rbuffer, wbuffer, size) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Resize too big ---"
+tests/test.py << TEST
+    lfs_format(&lfs, &cfg) => 0;
+
+    lfs_mount(&lfs, &cfg) => 0;
+    memset(buffer, 'm', 200);
+    buffer[200] = '\0';
+
+    size = 40;
+    lfs_file_open(&lfs, &file[0], (char*)buffer,
+            LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
+    memset(wbuffer, 'c', size);
+    lfs_file_write(&lfs, &file[0], wbuffer, size) => size;
+    lfs_file_close(&lfs, &file[0]) => 0;
+
+    size = 40;
+    lfs_file_open(&lfs, &file[0], (char*)buffer, LFS_O_RDONLY) => 0;
+    lfs_file_read(&lfs, &file[0], rbuffer, size) => size;
+    memcmp(rbuffer, wbuffer, size) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+
+    size = 400;
+    lfs_file_open(&lfs, &file[0], (char*)buffer,
+            LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
+    memset(wbuffer, 'c', size);
+    lfs_file_write(&lfs, &file[0], wbuffer, size) => size;
+    lfs_file_close(&lfs, &file[0]) => 0;
+
+    size = 400;
+    lfs_file_open(&lfs, &file[0], (char*)buffer, LFS_O_RDONLY) => 0;
+    lfs_file_read(&lfs, &file[0], rbuffer, size) => size;
+    memcmp(rbuffer, wbuffer, size) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Results ---"
+tests/stats.py

+ 7 - 7
tests/test_files.sh

@@ -29,7 +29,7 @@ tests/test.py << TEST
 TEST
 
 w_test() {
-tests/test.py << TEST
+tests/test.py ${4:-} << TEST
     size = $1;
     lfs_size_t chunk = 31;
     srand(0);
@@ -115,21 +115,21 @@ tests/test.py << TEST
     info.type => LFS_TYPE_REG;
     info.size => strlen("Hello World!\n");
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "smallavacado") => 0;
+    strcmp(info.name, "largeavacado") => 0;
     info.type => LFS_TYPE_REG;
-    info.size => $SMALLSIZE;
+    info.size => $LARGESIZE;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "mediumavacado") => 0;
     info.type => LFS_TYPE_REG;
     info.size => $MEDIUMSIZE;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    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) => 1;
+    strcmp(info.name, "smallavacado") => 0;
+    info.type => LFS_TYPE_REG;
+    info.size => $SMALLSIZE;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_dir_close(&lfs, &dir[0]) => 0;
     lfs_unmount(&lfs) => 0;

+ 15 - 14
tests/test_format.sh

@@ -9,39 +9,40 @@ tests/test.py << TEST
     lfs_format(&lfs, &cfg) => 0;
 TEST
 
-echo "--- Invalid superblocks ---"
-ln -f -s /dev/zero blocks/0
-ln -f -s /dev/zero blocks/1
-tests/test.py << TEST
-    lfs_format(&lfs, &cfg) => LFS_ERR_CORRUPT;
-TEST
-rm blocks/0 blocks/1
-
 echo "--- Basic mounting ---"
 tests/test.py << TEST
     lfs_format(&lfs, &cfg) => 0;
-TEST
-tests/test.py << TEST
+
     lfs_mount(&lfs, &cfg) => 0;
     lfs_unmount(&lfs) => 0;
 TEST
 
-echo "--- Invalid mount ---"
+echo "--- Invalid superblocks ---"
+ln -f -s /dev/zero blocks/0
+ln -f -s /dev/zero blocks/1
 tests/test.py << TEST
-    lfs_format(&lfs, &cfg) => 0;
+    lfs_format(&lfs, &cfg) => LFS_ERR_NOSPC;
 TEST
 rm blocks/0 blocks/1
+
+echo "--- Invalid mount ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => LFS_ERR_CORRUPT;
 TEST
 
-echo "--- Valid corrupt mount ---"
+echo "--- Expanding superblock ---"
 tests/test.py << TEST
     lfs_format(&lfs, &cfg) => 0;
+    lfs_mount(&lfs, &cfg) => 0;
+    for (int i = 0; i < 100; i++) {
+        lfs_mkdir(&lfs, "dummy") => 0;
+        lfs_remove(&lfs, "dummy") => 0;
+    }
+    lfs_unmount(&lfs) => 0;
 TEST
-rm blocks/0
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
+    lfs_mkdir(&lfs, "dummy") => 0;
     lfs_unmount(&lfs) => 0;
 TEST
 

+ 110 - 14
tests/test_move.sh

@@ -59,7 +59,7 @@ tests/test.py << TEST
     lfs_rename(&lfs, "b/hello", "c/hello") => 0;
     lfs_unmount(&lfs) => 0;
 TEST
-rm -v blocks/7
+tests/corrupt.py -n 1
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     lfs_dir_open(&lfs, &dir[0], "b") => 0;
@@ -86,8 +86,7 @@ tests/test.py << TEST
     lfs_rename(&lfs, "c/hello", "d/hello") => 0;
     lfs_unmount(&lfs) => 0;
 TEST
-rm -v blocks/8
-rm -v blocks/a
+tests/corrupt.py -n 2
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     lfs_dir_open(&lfs, &dir[0], "c") => 0;
@@ -108,6 +107,32 @@ tests/test.py << TEST
     lfs_unmount(&lfs) => 0;
 TEST
 
+echo "--- Move file after corrupt ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_rename(&lfs, "c/hello", "d/hello") => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_dir_open(&lfs, &dir[0], "c") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, ".") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "..") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 0;
+    lfs_dir_close(&lfs, &dir[0]) => 0;
+    lfs_dir_open(&lfs, &dir[0], "d") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, ".") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "..") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "hello") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+
 echo "--- Move dir ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
@@ -140,7 +165,7 @@ tests/test.py << TEST
     lfs_rename(&lfs, "b/hi", "c/hi") => 0;
     lfs_unmount(&lfs) => 0;
 TEST
-rm -v blocks/7
+tests/corrupt.py -n 1
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     lfs_dir_open(&lfs, &dir[0], "b") => 0;
@@ -156,8 +181,6 @@ tests/test.py << TEST
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "..") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "hello") => 0;
-    lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "hi") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_unmount(&lfs) => 0;
@@ -169,8 +192,7 @@ tests/test.py << TEST
     lfs_rename(&lfs, "c/hi", "d/hi") => 0;
     lfs_unmount(&lfs) => 0;
 TEST
-rm -v blocks/9
-rm -v blocks/a
+tests/corrupt.py -n 2
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     lfs_dir_open(&lfs, &dir[0], "c") => 0;
@@ -179,9 +201,33 @@ tests/test.py << TEST
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "..") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "hi") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 0;
+    lfs_dir_close(&lfs, &dir[0]) => 0;
+    lfs_dir_open(&lfs, &dir[0], "d") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, ".") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "..") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "hello") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Move dir after corrupt ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_rename(&lfs, "c/hi", "d/hi") => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_dir_open(&lfs, &dir[0], "c") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
-    strcmp(info.name, "hi") => 0;
+    strcmp(info.name, ".") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "..") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_dir_close(&lfs, &dir[0]) => 0;
     lfs_dir_open(&lfs, &dir[0], "d") => 0;
@@ -189,6 +235,10 @@ tests/test.py << TEST
     strcmp(info.name, ".") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "..") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "hello") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "hi") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_unmount(&lfs) => 0;
 TEST
@@ -199,27 +249,73 @@ tests/test.py << TEST
 
     lfs_dir_open(&lfs, &dir[0], "a/hi") => LFS_ERR_NOENT;
     lfs_dir_open(&lfs, &dir[0], "b/hi") => LFS_ERR_NOENT;
-    lfs_dir_open(&lfs, &dir[0], "d/hi") => LFS_ERR_NOENT;
+    lfs_dir_open(&lfs, &dir[0], "c/hi") => LFS_ERR_NOENT;
 
-    lfs_dir_open(&lfs, &dir[0], "c/hi") => 0;
+    lfs_dir_open(&lfs, &dir[0], "d/hi") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, ".") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "..") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "bonjour") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "hola") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "ohayo") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 0;
+    lfs_dir_close(&lfs, &dir[0]) => 0;
+
+    lfs_dir_open(&lfs, &dir[0], "a/hello") => LFS_ERR_NOENT;
+    lfs_dir_open(&lfs, &dir[0], "b/hello") => LFS_ERR_NOENT;
+    lfs_dir_open(&lfs, &dir[0], "c/hello") => LFS_ERR_NOENT;
+
+    lfs_file_open(&lfs, &file[0], "d/hello", LFS_O_RDONLY) => 0;
+    lfs_file_read(&lfs, &file[0], buffer, 5) => 5;
+    memcmp(buffer, "hola\n", 5) => 0;
+    lfs_file_read(&lfs, &file[0], buffer, 8) => 8;
+    memcmp(buffer, "bonjour\n", 8) => 0;
+    lfs_file_read(&lfs, &file[0], buffer, 6) => 6;
+    memcmp(buffer, "ohayo\n", 6) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Move state stealing ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+
+    lfs_remove(&lfs, "b") => 0;
+    lfs_remove(&lfs, "c") => 0;
+
+    lfs_unmount(&lfs) => 0;
+TEST
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+
+    lfs_dir_open(&lfs, &dir[0], "a/hi") => LFS_ERR_NOENT;
+    lfs_dir_open(&lfs, &dir[0], "b") => LFS_ERR_NOENT;
+    lfs_dir_open(&lfs, &dir[0], "c") => LFS_ERR_NOENT;
+
+    lfs_dir_open(&lfs, &dir[0], "d/hi") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, ".") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "..") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "bonjour") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
+    strcmp(info.name, "hola") => 0;
+    lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, "ohayo") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 0;
     lfs_dir_close(&lfs, &dir[0]) => 0;
 
     lfs_dir_open(&lfs, &dir[0], "a/hello") => LFS_ERR_NOENT;
-    lfs_dir_open(&lfs, &dir[0], "b/hello") => LFS_ERR_NOENT;
-    lfs_dir_open(&lfs, &dir[0], "d/hello") => LFS_ERR_NOENT;
+    lfs_dir_open(&lfs, &dir[0], "b") => LFS_ERR_NOENT;
+    lfs_dir_open(&lfs, &dir[0], "c") => LFS_ERR_NOENT;
 
-    lfs_file_open(&lfs, &file[0], "c/hello", LFS_O_RDONLY) => 0;
+    lfs_file_open(&lfs, &file[0], "d/hello", LFS_O_RDONLY) => 0;
     lfs_file_read(&lfs, &file[0], buffer, 5) => 5;
     memcmp(buffer, "hola\n", 5) => 0;
     lfs_file_read(&lfs, &file[0], buffer, 8) => 8;

+ 15 - 11
tests/test_orphan.sh

@@ -15,25 +15,29 @@ tests/test.py << TEST
     lfs_mkdir(&lfs, "parent/child") => 0;
     lfs_remove(&lfs, "parent/orphan") => 0;
 TEST
-# remove most recent file, this should be the update to the previous
+# corrupt most recent commit, this should be the update to the previous
 # linked-list entry and should orphan the child
-rm -v blocks/8
+tests/corrupt.py
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
+
+    lfs_stat(&lfs, "parent/orphan", &info) => LFS_ERR_NOENT;
+    lfs_ssize_t before = lfs_fs_size(&lfs);
+    before => 8;
+
+    lfs_unmount(&lfs) => 0;
+    lfs_mount(&lfs, &cfg) => 0;
+
     lfs_stat(&lfs, "parent/orphan", &info) => LFS_ERR_NOENT;
-    unsigned before = 0;
-    lfs_traverse(&lfs, test_count, &before) => 0;
-    test_log("before", before);
+    lfs_ssize_t orphaned = lfs_fs_size(&lfs);
+    orphaned => 8;
 
-    lfs_deorphan(&lfs) => 0;
+    lfs_mkdir(&lfs, "parent/otherchild") => 0;
 
     lfs_stat(&lfs, "parent/orphan", &info) => LFS_ERR_NOENT;
-    unsigned after = 0;
-    lfs_traverse(&lfs, test_count, &after) => 0;
-    test_log("after", after);
+    lfs_ssize_t deorphaned = lfs_fs_size(&lfs);
+    deorphaned => 8;
 
-    int diff = before - after;
-    diff => 2;
     lfs_unmount(&lfs) => 0;
 TEST
 

+ 58 - 0
tests/test_paths.sh

@@ -128,6 +128,14 @@ tests/test.py << TEST
     lfs_mkdir(&lfs, "/") => LFS_ERR_EXIST;
     lfs_file_open(&lfs, &file[0], "/", LFS_O_WRONLY | LFS_O_CREAT)
         => LFS_ERR_ISDIR;
+
+    // more corner cases
+    lfs_remove(&lfs, "") => LFS_ERR_INVAL;
+    lfs_remove(&lfs, ".") => LFS_ERR_INVAL;
+    lfs_remove(&lfs, "..") => LFS_ERR_INVAL;
+    lfs_remove(&lfs, "/") => LFS_ERR_INVAL;
+    lfs_remove(&lfs, "//") => LFS_ERR_INVAL;
+    lfs_remove(&lfs, "./") => LFS_ERR_INVAL;
     lfs_unmount(&lfs) => 0;
 TEST
 
@@ -139,5 +147,55 @@ tests/test.py << TEST
     lfs_unmount(&lfs) => 0;
 TEST
 
+echo "--- Superblock conflict test ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    lfs_mkdir(&lfs, "littlefs") => 0;
+    lfs_remove(&lfs, "littlefs") => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Max path test ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    memset(buffer, 'w', LFS_NAME_MAX+1);
+    buffer[LFS_NAME_MAX+2] = '\0';
+    lfs_mkdir(&lfs, (char*)buffer) => LFS_ERR_NAMETOOLONG;
+    lfs_file_open(&lfs, &file[0], (char*)buffer,
+            LFS_O_WRONLY | LFS_O_CREAT) => LFS_ERR_NAMETOOLONG;
+
+    memcpy(buffer, "coffee/", strlen("coffee/"));
+    memset(buffer+strlen("coffee/"), 'w', LFS_NAME_MAX+1);
+    buffer[strlen("coffee/")+LFS_NAME_MAX+2] = '\0';
+    lfs_mkdir(&lfs, (char*)buffer) => LFS_ERR_NAMETOOLONG;
+    lfs_file_open(&lfs, &file[0], (char*)buffer,
+            LFS_O_WRONLY | LFS_O_CREAT) => LFS_ERR_NAMETOOLONG;
+    lfs_unmount(&lfs) => 0;
+TEST
+
+echo "--- Really big path test ---"
+tests/test.py << TEST
+    lfs_mount(&lfs, &cfg) => 0;
+    memset(buffer, 'w', LFS_NAME_MAX);
+    buffer[LFS_NAME_MAX+1] = '\0';
+    lfs_mkdir(&lfs, (char*)buffer) => 0;
+    lfs_remove(&lfs, (char*)buffer) => 0;
+    lfs_file_open(&lfs, &file[0], (char*)buffer,
+            LFS_O_WRONLY | LFS_O_CREAT) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    lfs_remove(&lfs, (char*)buffer) => 0;
+
+    memcpy(buffer, "coffee/", strlen("coffee/"));
+    memset(buffer+strlen("coffee/"), 'w', LFS_NAME_MAX);
+    buffer[strlen("coffee/")+LFS_NAME_MAX+1] = '\0';
+    lfs_mkdir(&lfs, (char*)buffer) => 0;
+    lfs_remove(&lfs, (char*)buffer) => 0;
+    lfs_file_open(&lfs, &file[0], (char*)buffer,
+            LFS_O_WRONLY | LFS_O_CREAT) => 0;
+    lfs_file_close(&lfs, &file[0]) => 0;
+    lfs_remove(&lfs, (char*)buffer) => 0;
+    lfs_unmount(&lfs) => 0;
+TEST
+
 echo "--- Results ---"
 tests/stats.py

+ 15 - 15
tests/test_seek.sh

@@ -12,7 +12,7 @@ tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
     lfs_mkdir(&lfs, "hello") => 0;
     for (int i = 0; i < $LARGESIZE; i++) {
-        sprintf((char*)buffer, "hello/kitty%d", i);
+        sprintf((char*)buffer, "hello/kitty%03d", i);
         lfs_file_open(&lfs, &file[0], (char*)buffer,
                 LFS_O_WRONLY | LFS_O_CREAT | LFS_O_APPEND) => 0;
 
@@ -39,7 +39,7 @@ tests/test.py << TEST
     lfs_soff_t pos;
     int i;
     for (i = 0; i < $SMALLSIZE; i++) {
-        sprintf((char*)buffer, "kitty%d", i);
+        sprintf((char*)buffer, "kitty%03d", i);
         lfs_dir_read(&lfs, &dir[0], &info) => 1;
         strcmp(info.name, (char*)buffer) => 0;
         pos = lfs_dir_tell(&lfs, &dir[0]);
@@ -47,12 +47,12 @@ tests/test.py << TEST
     pos >= 0 => 1;
 
     lfs_dir_seek(&lfs, &dir[0], pos) => 0;
-    sprintf((char*)buffer, "kitty%d", i);
+    sprintf((char*)buffer, "kitty%03d", i);
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, (char*)buffer) => 0;
 
     lfs_dir_rewind(&lfs, &dir[0]) => 0;
-    sprintf((char*)buffer, "kitty%d", 0);
+    sprintf((char*)buffer, "kitty%03d", 0);
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, ".") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
@@ -61,7 +61,7 @@ tests/test.py << TEST
     strcmp(info.name, (char*)buffer) => 0;
 
     lfs_dir_seek(&lfs, &dir[0], pos) => 0;
-    sprintf((char*)buffer, "kitty%d", i);
+    sprintf((char*)buffer, "kitty%03d", i);
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, (char*)buffer) => 0;
 
@@ -81,7 +81,7 @@ tests/test.py << TEST
     lfs_soff_t pos;
     int i;
     for (i = 0; i < $MEDIUMSIZE; i++) {
-        sprintf((char*)buffer, "kitty%d", i);
+        sprintf((char*)buffer, "kitty%03d", i);
         lfs_dir_read(&lfs, &dir[0], &info) => 1;
         strcmp(info.name, (char*)buffer) => 0;
         pos = lfs_dir_tell(&lfs, &dir[0]);
@@ -89,12 +89,12 @@ tests/test.py << TEST
     pos >= 0 => 1;
 
     lfs_dir_seek(&lfs, &dir[0], pos) => 0;
-    sprintf((char*)buffer, "kitty%d", i);
+    sprintf((char*)buffer, "kitty%03d", i);
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, (char*)buffer) => 0;
 
     lfs_dir_rewind(&lfs, &dir[0]) => 0;
-    sprintf((char*)buffer, "kitty%d", 0);
+    sprintf((char*)buffer, "kitty%03d", 0);
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, ".") => 0;
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
@@ -103,7 +103,7 @@ tests/test.py << TEST
     strcmp(info.name, (char*)buffer) => 0;
 
     lfs_dir_seek(&lfs, &dir[0], pos) => 0;
-    sprintf((char*)buffer, "kitty%d", i);
+    sprintf((char*)buffer, "kitty%03d", i);
     lfs_dir_read(&lfs, &dir[0], &info) => 1;
     strcmp(info.name, (char*)buffer) => 0;
 
@@ -114,7 +114,7 @@ TEST
 echo "--- Simple file seek ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
-    lfs_file_open(&lfs, &file[0], "hello/kitty42", LFS_O_RDONLY) => 0;
+    lfs_file_open(&lfs, &file[0], "hello/kitty042", LFS_O_RDONLY) => 0;
 
     lfs_soff_t pos;
     size = strlen("kittycatcat");
@@ -163,7 +163,7 @@ TEST
 echo "--- Large file seek ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
-    lfs_file_open(&lfs, &file[0], "hello/kitty42", LFS_O_RDONLY) => 0;
+    lfs_file_open(&lfs, &file[0], "hello/kitty042", LFS_O_RDONLY) => 0;
 
     lfs_soff_t pos;
     size = strlen("kittycatcat");
@@ -212,7 +212,7 @@ TEST
 echo "--- Simple file seek and write ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
-    lfs_file_open(&lfs, &file[0], "hello/kitty42", LFS_O_RDWR) => 0;
+    lfs_file_open(&lfs, &file[0], "hello/kitty042", LFS_O_RDWR) => 0;
 
     lfs_soff_t pos;
     size = strlen("kittycatcat");
@@ -253,7 +253,7 @@ TEST
 echo "--- Large file seek and write ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
-    lfs_file_open(&lfs, &file[0], "hello/kitty42", LFS_O_RDWR) => 0;
+    lfs_file_open(&lfs, &file[0], "hello/kitty042", LFS_O_RDWR) => 0;
 
     lfs_soff_t pos;
     size = strlen("kittycatcat");
@@ -296,7 +296,7 @@ TEST
 echo "--- Boundary seek and write ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
-    lfs_file_open(&lfs, &file[0], "hello/kitty42", LFS_O_RDWR) => 0;
+    lfs_file_open(&lfs, &file[0], "hello/kitty042", LFS_O_RDWR) => 0;
 
     size = strlen("hedgehoghog");
     const lfs_soff_t offsets[] = {512, 1020, 513, 1021, 511, 1019};
@@ -324,7 +324,7 @@ TEST
 echo "--- Out-of-bounds seek ---"
 tests/test.py << TEST
     lfs_mount(&lfs, &cfg) => 0;
-    lfs_file_open(&lfs, &file[0], "hello/kitty42", LFS_O_RDWR) => 0;
+    lfs_file_open(&lfs, &file[0], "hello/kitty042", LFS_O_RDWR) => 0;
 
     size = strlen("kittycatcat");
     lfs_file_size(&lfs, &file[0]) => $LARGESIZE*size;

Some files were not shown because too many files changed in this diff