Răsfoiți Sursa

Add files via upload

Files commit
lorol 4 ani în urmă
părinte
comite
4e1979d7a1
11 a modificat fișierele cu 7942 adăugiri și 0 ștergeri
  1. 182 0
      examples/LittleFS_test/LittleFS_test.ino
  2. 130 0
      src/LITTLEFS.cpp
  3. 38 0
      src/LITTLEFS.h
  4. 1479 0
      src/esp_littlefs.c
  5. 114 0
      src/esp_littlefs.h
  6. 4913 0
      src/lfs.c
  7. 655 0
      src/lfs.h
  8. 33 0
      src/lfs_util.c
  9. 234 0
      src/lfs_util.h
  10. 58 0
      src/littlefs_api.c
  11. 106 0
      src/littlefs_api.h

+ 182 - 0
examples/LittleFS_test/LittleFS_test.ino

@@ -0,0 +1,182 @@
+#include <Arduino.h>
+#include "FS.h"
+#include <LITTLEFS.h>
+
+/* You only need to format SPIFFS the first time you run a
+   test or else use the LITTLEFS plugin to create a partition
+   https://github.com/me-no-dev/arduino-esp32fs-plugin */
+#define FORMAT_LITTLEFS_IF_FAILED true
+
+void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
+    Serial.printf("Listing directory: %s\r\n", dirname);
+
+    File root = fs.open(dirname);
+    if(!root){
+        Serial.println("- failed to open directory");
+        return;
+    }
+    if(!root.isDirectory()){
+        Serial.println(" - not a directory");
+        return;
+    }
+
+    File file = root.openNextFile();
+    while(file){
+        if(file.isDirectory()){
+            Serial.print("  DIR : ");
+            Serial.println(file.name());
+            if(levels){
+                listDir(fs, file.name(), levels -1);
+            }
+        } else {
+            Serial.print("  FILE: ");
+            Serial.print(file.name());
+            Serial.print("\tSIZE: ");
+            Serial.println(file.size());
+        }
+        file = root.openNextFile();
+    }
+}
+
+void readFile(fs::FS &fs, const char * path){
+    Serial.printf("Reading file: %s\r\n", path);
+
+    File file = fs.open(path);
+    if(!file || file.isDirectory()){
+        Serial.println("- failed to open file for reading");
+        return;
+    }
+
+    Serial.println("- read from file:");
+    while(file.available()){
+        Serial.write(file.read());
+    }
+    file.close();
+}
+
+void writeFile(fs::FS &fs, const char * path, const char * message){
+    Serial.printf("Writing file: %s\r\n", path);
+
+    File file = fs.open(path, FILE_WRITE);
+    if(!file){
+        Serial.println("- failed to open file for writing");
+        return;
+    }
+    if(file.print(message)){
+        Serial.println("- file written");
+    } else {
+        Serial.println("- write failed");
+    }
+    file.close();
+}
+
+void appendFile(fs::FS &fs, const char * path, const char * message){
+    Serial.printf("Appending to file: %s\r\n", path);
+
+    File file = fs.open(path, FILE_APPEND);
+    if(!file){
+        Serial.println("- failed to open file for appending");
+        return;
+    }
+    if(file.print(message)){
+        Serial.println("- message appended");
+    } else {
+        Serial.println("- append failed");
+    }
+    file.close();
+}
+
+void renameFile(fs::FS &fs, const char * path1, const char * path2){
+    Serial.printf("Renaming file %s to %s\r\n", path1, path2);
+    if (fs.rename(path1, path2)) {
+        Serial.println("- file renamed");
+    } else {
+        Serial.println("- rename failed");
+    }
+}
+
+void deleteFile(fs::FS &fs, const char * path){
+    Serial.printf("Deleting file: %s\r\n", path);
+    if(fs.remove(path)){
+        Serial.println("- file deleted");
+    } else {
+        Serial.println("- delete failed");
+    }
+}
+
+void testFileIO(fs::FS &fs, const char * path){
+    Serial.printf("Testing file I/O with %s\r\n", path);
+
+    static uint8_t buf[512];
+    size_t len = 0;
+    File file = fs.open(path, FILE_WRITE);
+    if(!file){
+        Serial.println("- failed to open file for writing");
+        return;
+    }
+
+    size_t i;
+    Serial.print("- writing" );
+    uint32_t start = millis();
+    for(i=0; i<2048; i++){
+        if ((i & 0x001F) == 0x001F){
+          Serial.print(".");
+        }
+        file.write(buf, 512);
+    }
+    Serial.println("");
+    uint32_t end = millis() - start;
+    Serial.printf(" - %u bytes written in %u ms\r\n", 2048 * 512, end);
+    file.close();
+
+    file = fs.open(path);
+    start = millis();
+    end = start;
+    i = 0;
+    if(file && !file.isDirectory()){
+        len = file.size();
+        size_t flen = len;
+        start = millis();
+        Serial.print("- reading" );
+        while(len){
+            size_t toRead = len;
+            if(toRead > 512){
+                toRead = 512;
+            }
+            file.read(buf, toRead);
+            if ((i++ & 0x001F) == 0x001F){
+              Serial.print(".");
+            }
+            len -= toRead;
+        }
+        Serial.println("");
+        end = millis() - start;
+        Serial.printf("- %u bytes read in %u ms\r\n", flen, end);
+        file.close();
+    } else {
+        Serial.println("- failed to open file for reading");
+    }
+}
+
+void setup(){
+    Serial.begin(115200);
+    if(!LITTLEFS.begin(FORMAT_LITTLEFS_IF_FAILED)){
+        Serial.println("LITTLEFS Mount Failed");
+        return;
+    }
+    
+    listDir(LITTLEFS, "/", 0);
+    writeFile(LITTLEFS, "/hello.txt", "Hello ");
+    appendFile(LITTLEFS, "/hello.txt", "World!\r\n");
+    readFile(LITTLEFS, "/hello.txt");
+    renameFile(LITTLEFS, "/hello.txt", "/foo.txt");
+    readFile(LITTLEFS, "/foo.txt");
+    deleteFile(LITTLEFS, "/foo.txt");
+    testFileIO(LITTLEFS, "/test.txt");
+    deleteFile(LITTLEFS, "/test.txt");
+    Serial.println( "Test complete" );
+}
+
+void loop(){
+
+}

+ 130 - 0
src/LITTLEFS.cpp

@@ -0,0 +1,130 @@
+// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//esp_err_t esp_vfs_littlefs_register(const esp_vfs_littlefs_conf_t * conf);
+
+//esp_err_t esp_vfs_littlefs_unregister(const char* partition_label);
+//esp_err_t esp_littlefs_format(const char* partition_label);
+//esp_err_t esp_littlefs_info(const char* partition_label, size_t *total_bytes, size_t *used_bytes);
+
+#define LFS_NAME "spiffs"
+
+#include "vfs_api.h"
+
+extern "C" {
+#include <sys/unistd.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include "esp_littlefs.h"
+}
+
+#include "LITTLEFS.h"
+
+using namespace fs;
+
+class LITTLEFSImpl : public VFSImpl
+{
+public:
+    LITTLEFSImpl();
+    virtual ~LITTLEFSImpl() { }
+    virtual bool exists(const char* path);
+};
+
+LITTLEFSImpl::LITTLEFSImpl()
+{
+}
+
+bool LITTLEFSImpl::exists(const char* path)
+{
+    File f = open(path, "r");
+    return (f == true) && !f.isDirectory();
+}
+
+LITTLEFSFS::LITTLEFSFS() : FS(FSImplPtr(new LITTLEFSImpl()))
+{
+
+}
+
+bool LITTLEFSFS::begin(bool formatOnFail, const char * basePath, uint8_t maxOpenFiles)
+{
+    if(esp_littlefs_mounted(LFS_NAME)){
+        log_w("LITTLEFS Already Mounted!");
+        return true;
+    }
+
+    esp_vfs_littlefs_conf_t conf = {
+      .base_path = basePath,
+      .partition_label = LFS_NAME,
+      //.max_files = maxOpenFiles,
+      .format_if_mount_failed = false
+    };
+
+    esp_err_t err = esp_vfs_littlefs_register(&conf);
+    if(err == ESP_FAIL && formatOnFail){
+        if(format()){
+            err = esp_vfs_littlefs_register(&conf);
+        }
+    }
+    if(err != ESP_OK){
+        log_e("Mounting LITTLEFS failed! Error: %d", err);
+        return false;
+    }
+    _impl->mountpoint(basePath);
+    return true;
+}
+
+void LITTLEFSFS::end()
+{
+    if(esp_littlefs_mounted(LFS_NAME)){
+        esp_err_t err = esp_vfs_littlefs_unregister(LFS_NAME);
+        if(err){
+            log_e("Unmounting LITTLEFS failed! Error: %d", err);
+            return;
+        }
+        _impl->mountpoint(NULL);
+    }
+}
+
+bool LITTLEFSFS::format()
+{
+    disableCore0WDT();
+    esp_err_t err = esp_littlefs_format(LFS_NAME);
+    enableCore0WDT();
+    if(err){
+        log_e("Formatting LITTLEFS failed! Error: %d", err);
+        return false;
+    }
+    return true;
+}
+
+size_t LITTLEFSFS::totalBytes()
+{
+    size_t total,used;
+    if(esp_littlefs_info(LFS_NAME, &total, &used)){
+        return 0;
+    }
+    return total;
+}
+
+size_t LITTLEFSFS::usedBytes()
+{
+    size_t total,used;
+    if(esp_littlefs_info(LFS_NAME, &total, &used)){
+        return 0;
+    }
+    return used;
+}
+
+LITTLEFSFS LITTLEFS;
+

+ 38 - 0
src/LITTLEFS.h

@@ -0,0 +1,38 @@
+// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#ifndef _LITTLEFS_H_
+#define _LITTLEFS_H_
+
+#include "FS.h"
+
+namespace fs
+{
+
+class LITTLEFSFS : public FS
+{
+public:
+    LITTLEFSFS();
+    bool begin(bool formatOnFail=false, const char * basePath="/littlefs", uint8_t maxOpenFiles=5);
+    bool format();
+    size_t totalBytes();
+    size_t usedBytes();
+    void end();
+};
+
+}
+
+extern fs::LITTLEFSFS LITTLEFS;
+
+
+#endif

+ 1479 - 0
src/esp_littlefs.c

@@ -0,0 +1,1479 @@
+/**
+ * @file esp_littlefs.c
+ * @brief Maps LittleFS <-> ESP_VFS 
+ * @author Brian Pugh
+ */
+
+//#define LOG_LOCAL_LEVEL 4
+
+#include "esp_log.h"
+#include "esp_spi_flash.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include <unistd.h>
+#include <dirent.h>
+#include <sys/errno.h>
+#include <sys/fcntl.h>
+#include <sys/lock.h>
+#include <sys/param.h>
+#include "rom/spi_flash.h"
+#include "esp_system.h"
+
+#include "esp_littlefs.h"
+#include "littlefs_api.h"
+
+
+static const char TAG[] = "esp_littlefs";
+
+#define CONFIG_LITTLEFS_BLOCK_SIZE 4096 /* ESP32 can only operate at 4kb */
+
+/* File Descriptor Caching Params */
+#define CONFIG_LITTLEFS_FD_CACHE_REALLOC_FACTOR 2  /* Amount to resize FD cache by */
+#define CONFIG_LITTLEFS_FD_CACHE_MIN_SIZE 4  /* Minimum size of FD cache */
+#define CONFIG_LITTLEFS_FD_CACHE_HYST 4  /* When shrinking, leave this many trailing FD slots available */
+
+#define CONFIG_LITTLEFS_MAX_PARTITIONS 3
+#define CONFIG_LITTLEFS_PAGE_SIZE 256
+#define CONFIG_LITTLEFS_OBJ_NAME_LEN 64
+#define CONFIG_LITTLEFS_READ_SIZE 128
+#define CONFIG_LITTLEFS_WRITE_SIZE 128
+#define CONFIG_LITTLEFS_LOOKAHEAD_SIZE 512
+#define CONFIG_LITTLEFS_CACHE_SIZE 128
+#define CONFIG_LITTLEFS_BLOCK_CYCLES 512
+#define CONFIG_LITTLEFS_USE_MTIME 1
+#define CONFIG_LITTLEFS_MTIME_USE_SECONDS 1
+
+
+
+/**
+ * @brief littlefs DIR structure
+ */
+typedef struct {
+    DIR dir;            /*!< VFS DIR struct */
+    lfs_dir_t d;        /*!< littlefs DIR struct */
+    struct dirent e;    /*!< Last open dirent */
+    long offset;        /*!< Offset of the current dirent */
+    char *path;         /*!< Requested directory name */
+} vfs_littlefs_dir_t;
+
+static int     vfs_littlefs_open(void* ctx, const char * path, int flags, int mode);
+static ssize_t vfs_littlefs_write(void* ctx, int fd, const void * data, size_t size);
+static ssize_t vfs_littlefs_read(void* ctx, int fd, void * dst, size_t size);
+static int     vfs_littlefs_close(void* ctx, int fd);
+static off_t   vfs_littlefs_lseek(void* ctx, int fd, off_t offset, int mode);
+static int     vfs_littlefs_stat(void* ctx, const char * path, struct stat * st);
+static int     vfs_littlefs_unlink(void* ctx, const char *path);
+static int     vfs_littlefs_rename(void* ctx, const char *src, const char *dst);
+static DIR*    vfs_littlefs_opendir(void* ctx, const char* name);
+static int     vfs_littlefs_closedir(void* ctx, DIR* pdir);
+static struct  dirent* vfs_littlefs_readdir(void* ctx, DIR* pdir);
+static int     vfs_littlefs_readdir_r(void* ctx, DIR* pdir,
+                                struct dirent* entry, struct dirent** out_dirent);
+static long    vfs_littlefs_telldir(void* ctx, DIR* pdir);
+static void    vfs_littlefs_seekdir(void* ctx, DIR* pdir, long offset);
+static int     vfs_littlefs_mkdir(void* ctx, const char* name, mode_t mode);
+static int     vfs_littlefs_rmdir(void* ctx, const char* name);
+static int     vfs_littlefs_fsync(void* ctx, int fd);
+
+static esp_err_t esp_littlefs_init(const esp_vfs_littlefs_conf_t* conf);
+static esp_err_t esp_littlefs_erase_partition(const char *partition_label);
+static esp_err_t esp_littlefs_by_label(const char* label, int * index);
+static esp_err_t esp_littlefs_get_empty(int *index);
+static void      esp_littlefs_free(esp_littlefs_t ** efs);
+static void      esp_littlefs_dir_free(vfs_littlefs_dir_t *dir);
+static int       esp_littlefs_flags_conv(int m);
+#if CONFIG_LITTLEFS_USE_MTIME
+static int       vfs_littlefs_utime(void *ctx, const char *path, const struct utimbuf *times);
+static void      vfs_littlefs_update_mtime(esp_littlefs_t *efs, const char *path);
+static int       vfs_littlefs_update_mtime_value(esp_littlefs_t *efs, const char *path, time_t t);
+static time_t    vfs_littlefs_get_mtime(esp_littlefs_t *efs, const char *path);
+#endif
+
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+/* The only way in LittleFS to get info is via a path (lfs_stat), so it cannot
+ * be done if the path isn't stored. */
+static int     vfs_littlefs_fstat(void* ctx, int fd, struct stat * st);
+#endif
+
+static int sem_take(esp_littlefs_t *efs);
+static int sem_give(esp_littlefs_t *efs);
+
+static SemaphoreHandle_t _efs_lock = NULL;
+static esp_littlefs_t * _efs[CONFIG_LITTLEFS_MAX_PARTITIONS] = { 0 };
+
+/********************
+ * Helper Functions *
+ ********************/
+void esp_littlefs_free_fds(esp_littlefs_t * efs) {
+    /* Need to free all files that were opened */
+    while (efs->file) {
+        vfs_littlefs_file_t * next = efs->file->next;
+        free(efs->file);
+        efs->file = next;
+    }
+    free(efs->cache); 
+    efs->cache = 0;
+    efs->cache_size = efs->fd_count = 0;
+}
+
+
+/********************
+ * Public Functions *
+ ********************/
+
+bool esp_littlefs_mounted(const char* partition_label) {
+    int index;
+    esp_err_t err;
+
+    err = esp_littlefs_by_label(partition_label, &index);
+    if(err != ESP_OK) return false;
+    return _efs[index]->cache_size > 0;
+}
+
+esp_err_t esp_littlefs_info(const char* partition_label, size_t *total_bytes, size_t *used_bytes){
+    int index;
+    esp_err_t err;
+    esp_littlefs_t *efs = NULL;
+
+    err = esp_littlefs_by_label(partition_label, &index);
+    if(err != ESP_OK) return false;
+    efs = _efs[index];
+
+    if(total_bytes) *total_bytes = efs->cfg.block_size * efs->cfg.block_count; 
+    if(used_bytes) *used_bytes = efs->cfg.block_size * lfs_fs_size(efs->fs);
+
+    return ESP_OK;
+}
+
+esp_err_t esp_vfs_littlefs_register(const esp_vfs_littlefs_conf_t * conf)
+{
+    assert(conf->base_path);
+    const esp_vfs_t vfs = {
+        .flags       = ESP_VFS_FLAG_CONTEXT_PTR,
+        .write_p     = &vfs_littlefs_write,
+        .lseek_p     = &vfs_littlefs_lseek,
+        .read_p      = &vfs_littlefs_read,
+        .open_p      = &vfs_littlefs_open,
+        .close_p     = &vfs_littlefs_close,
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+        .fstat_p     = &vfs_littlefs_fstat,
+#else
+        .fstat_p     = NULL, /* Not supported */
+#endif
+        .stat_p      = &vfs_littlefs_stat,
+        .link_p      = NULL, /* Not Supported */
+        .unlink_p    = &vfs_littlefs_unlink,
+        .rename_p    = &vfs_littlefs_rename,
+        .opendir_p   = &vfs_littlefs_opendir,
+        .closedir_p  = &vfs_littlefs_closedir,
+        .readdir_p   = &vfs_littlefs_readdir,
+        .readdir_r_p = &vfs_littlefs_readdir_r,
+        .seekdir_p   = &vfs_littlefs_seekdir,
+        .telldir_p   = &vfs_littlefs_telldir,
+        .mkdir_p     = &vfs_littlefs_mkdir,
+        .rmdir_p     = &vfs_littlefs_rmdir,
+        .fsync_p     = &vfs_littlefs_fsync,
+#if CONFIG_LITTLEFS_USE_MTIME
+        .utime_p     = &vfs_littlefs_utime,
+#else
+        .utime_p     = NULL,
+#endif // CONFIG_LITTLEFS_USE_MTIME
+    };
+
+    esp_err_t err = esp_littlefs_init(conf);
+    if (err != ESP_OK) {
+        ESP_LOGE(TAG, "Failed to initialize LittleFS");
+        return err;
+    }
+
+    int index;
+    if (esp_littlefs_by_label(conf->partition_label, &index) != ESP_OK) {
+        ESP_LOGE(TAG, "Unable to find partition \"%s\"", conf->partition_label);
+        return ESP_ERR_NOT_FOUND;
+    }
+
+    strlcat(_efs[index]->base_path, conf->base_path, ESP_VFS_PATH_MAX + 1);
+    err = esp_vfs_register(conf->base_path, &vfs, _efs[index]);
+    if (err != ESP_OK) {
+        esp_littlefs_free(&_efs[index]);
+        ESP_LOGE(TAG, "Failed to register Littlefs to \"%s\"", conf->base_path);
+        return err;
+    }
+
+    ESP_LOGD(TAG, "Successfully registered LittleFS to \"%s\"", conf->base_path);
+    return ESP_OK;
+}
+
+esp_err_t esp_vfs_littlefs_unregister(const char* partition_label)
+{
+    assert(partition_label);
+    int index;
+    if (esp_littlefs_by_label(partition_label, &index) != ESP_OK) {
+        ESP_LOGE(TAG, "Partition was never registered.");
+        return ESP_ERR_INVALID_STATE;
+    }
+    ESP_LOGD(TAG, "Unregistering \"%s\"", partition_label);
+    esp_err_t err = esp_vfs_unregister(_efs[index]->base_path);
+    if (err != ESP_OK) {
+        ESP_LOGE(TAG, "Failed to unregister \"%s\"", partition_label);
+        return err;
+    }
+    esp_littlefs_free(&_efs[index]);
+    _efs[index] = NULL;
+    return ESP_OK;
+}
+
+esp_err_t esp_littlefs_format(const char* partition_label) {
+    assert( partition_label );
+
+    bool was_mounted = false;
+    bool efs_free = false;
+    int index = -1;
+    esp_err_t err;
+    esp_littlefs_t *efs = NULL;
+
+    ESP_LOGI(TAG, "Formatting \"%s\"", partition_label);
+
+    /* Get a context */
+    err = esp_littlefs_by_label(partition_label, &index);
+
+    if( err != ESP_OK ){
+        /* Create a tmp context */
+        ESP_LOGD(TAG, "Temporarily creating EFS context.");
+        efs_free = true;
+        const esp_vfs_littlefs_conf_t conf = {
+                /* base_name not necessary for initializing */
+                .dont_mount = true, 
+                .partition_label = partition_label,
+        };
+        err = esp_littlefs_init(&conf); /* Internally MIGHT call esp_littlefs_format */
+        if( err != ESP_OK ) {
+            ESP_LOGE(TAG, "Failed to initialize to format.");
+            goto exit;
+        }
+
+        err = esp_littlefs_by_label(partition_label, &index);
+        if ( err != ESP_OK) {
+            ESP_LOGE(TAG, "Error obtaining context.");
+            goto exit;
+        }
+    }
+
+    efs = _efs[index];
+    assert( efs );
+
+    /* Unmount if mounted */
+    if(efs->cache_size > 0){
+        int res;
+        ESP_LOGD(TAG, "Partition was mounted. Unmounting...");
+        was_mounted = true;
+        res = lfs_unmount(efs->fs);
+        if(res != LFS_ERR_OK){
+            ESP_LOGE(TAG, "Failed to unmount.");
+            return ESP_FAIL;
+        }
+        esp_littlefs_free_fds(efs);
+    }
+
+    /* Erase and Format */
+    {
+        int res;
+        ESP_LOGD(TAG, "Formatting filesystem");
+        esp_littlefs_erase_partition(partition_label);
+        res = lfs_format(efs->fs, &efs->cfg);
+        if( res != LFS_ERR_OK ) {
+            ESP_LOGE(TAG, "Failed to format filesystem");
+            return ESP_FAIL;
+        }
+    }
+
+    /* Mount filesystem */
+    if( was_mounted ) {
+        int res;
+        /* Remount the partition */
+        ESP_LOGD(TAG, "Remounting formatted partition");
+        res = lfs_mount(efs->fs, &efs->cfg);
+        if( res != LFS_ERR_OK ) {
+            ESP_LOGE(TAG, "Failed to re-mount filesystem");
+            return ESP_FAIL;
+        }
+        efs->cache_size = CONFIG_LITTLEFS_FD_CACHE_MIN_SIZE;  // Initial size of cache; will resize ondemand
+        efs->cache = calloc(sizeof(*efs->cache), efs->cache_size);
+    }
+    ESP_LOGD(TAG, "Format Success!");
+    
+    err = ESP_OK;
+
+exit:
+    if(efs_free && index>=0) esp_littlefs_free(&_efs[index]);
+    return err;
+}
+
+#if CONFIG_LITTLEFS_HUMAN_READABLE
+/**
+ * @brief converts an enumerated lfs error into a string.
+ * @param lfs_error The littlefs error.
+ */
+const char * esp_littlefs_errno(enum lfs_error lfs_errno) {
+    switch(lfs_errno){
+        case LFS_ERR_OK: return "LFS_ERR_OK";
+        case LFS_ERR_IO: return "LFS_ERR_IO";
+        case LFS_ERR_CORRUPT: return "LFS_ERR_CORRUPT";
+        case LFS_ERR_NOENT: return "LFS_ERR_NOENT";
+        case LFS_ERR_EXIST: return "LFS_ERR_EXIST";
+        case LFS_ERR_NOTDIR: return "LFS_ERR_NOTDIR";
+        case LFS_ERR_ISDIR: return "LFS_ERR_ISDIR";
+        case LFS_ERR_NOTEMPTY: return "LFS_ERR_NOTEMPTY";
+        case LFS_ERR_BADF: return "LFS_ERR_BADF";
+        case LFS_ERR_FBIG: return "LFS_ERR_FBIG";
+        case LFS_ERR_INVAL: return "LFS_ERR_INVAL";
+        case LFS_ERR_NOSPC: return "LFS_ERR_NOSPC";
+        case LFS_ERR_NOMEM: return "LFS_ERR_NOMEM";
+        case LFS_ERR_NOATTR: return "LFS_ERR_NOATTR";
+        case LFS_ERR_NAMETOOLONG: return "LFS_ERR_NAMETOOLONG";
+        default: return "LFS_ERR_UNDEFINED";
+    }
+    return "";
+}
+#else
+#define esp_littlefs_errno(x) ""
+#endif
+
+/********************
+ * Static Functions *
+ ********************/
+
+/*** Helpers ***/
+
+/**
+ * @brief Free and clear a littlefs definition structure.
+ * @param efs Pointer to pointer to struct. Done this way so we can also zero
+ *            out the pointer.
+ */
+static void esp_littlefs_free(esp_littlefs_t ** efs)
+{
+    esp_littlefs_t * e = *efs;
+    if (e == NULL) return;
+    *efs = NULL;
+
+    if (e->fs) {
+        if(e->cache_size > 0) lfs_unmount(e->fs);
+        free(e->fs);
+    }
+    if(e->lock) vSemaphoreDelete(e->lock);
+    esp_littlefs_free_fds(e);
+    free(e);
+}
+
+/**
+ * @brief Free a vfs_littlefs_dir_t struct.
+ */
+static void esp_littlefs_dir_free(vfs_littlefs_dir_t *dir){
+    if(dir == NULL) return;
+    if(dir->path) free(dir->path);
+    free(dir);
+}
+
+/**
+ * Get a mounted littlefs filesystem by label.
+ * @param[in] label
+ * @param[out] index index into _efs
+ * @return ESP_OK on success
+ */
+static esp_err_t esp_littlefs_by_label(const char* label, int * index){
+    int i;
+    esp_littlefs_t * p;
+
+    if(!label || !index) return ESP_ERR_INVALID_ARG;
+
+    ESP_LOGD(TAG, "Searching for existing filesystem for partition \"%s\"", label);
+
+    for (i = 0; i < CONFIG_LITTLEFS_MAX_PARTITIONS; i++) {
+        p = _efs[i];
+        if (p) {
+            if (strncmp(label, p->partition->label, 17) == 0) {
+                *index = i;
+                ESP_LOGD(TAG, "Found existing filesystem \"%s\" at index %d", label, *index);
+                return ESP_OK;
+            }
+        }
+    }
+
+    ESP_LOGD(TAG, "Existing filesystem \%s\" not found", label);
+    return ESP_ERR_NOT_FOUND;
+}
+
+/**
+ * @brief Get the index of an unallocated LittleFS slot.
+ * @param[out] index Indexd of free LittleFS slot 
+ * @return ESP_OK on success
+ */
+static esp_err_t esp_littlefs_get_empty(int *index) {
+    assert(index);
+    for(uint8_t i=0; i < CONFIG_LITTLEFS_MAX_PARTITIONS; i++){
+        if( _efs[i] == NULL ){
+            *index = i;
+            return ESP_OK;
+        }
+    }
+    ESP_LOGE(TAG, "No more free partitions available.");
+    return ESP_FAIL;
+}
+
+/**
+ * @brief erase a partition; make sure LittleFS is unmounted first.
+ * @param partition_label NULL-terminated string of partition to erase
+ * @return ESP_OK on success
+ */
+static esp_err_t esp_littlefs_erase_partition(const char *partition_label) {
+    ESP_LOGD(TAG, "Erasing partition...");
+
+    const esp_partition_t* partition = esp_partition_find_first(
+            ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY,
+            partition_label);
+    if (!partition) {
+        ESP_LOGE(TAG, "partition \"%s\" could not be found", partition_label);
+        return ESP_ERR_NOT_FOUND;
+    }
+
+    if( esp_partition_erase_range(partition, 0, partition->size) != ESP_OK ) {
+        ESP_LOGE(TAG, "Failed to erase partition");
+        return ESP_FAIL;
+    }
+
+    return ESP_OK;
+}
+
+/**
+ * @brief Convert fcntl flags to littlefs flags
+ * @param m fcntl flags
+ * @return lfs flags
+ */
+static int esp_littlefs_flags_conv(int m) {
+    int lfs_flags = 0;
+    if (m == O_APPEND) lfs_flags |= LFS_O_APPEND;
+    if (m == O_RDONLY) lfs_flags |= LFS_O_RDONLY;
+    if (m & O_WRONLY)  lfs_flags |= LFS_O_WRONLY;
+    if (m & O_RDWR)    lfs_flags |= LFS_O_RDWR;
+    if (m & O_EXCL)    lfs_flags |= LFS_O_EXCL;
+    if (m & O_CREAT)   lfs_flags |= LFS_O_CREAT;
+    if (m & O_TRUNC)   lfs_flags |= LFS_O_TRUNC;
+    return lfs_flags;
+}
+
+/**
+ * @brief Initialize and mount littlefs 
+ * @param[in] conf Filesystem Configuration
+ * @return ESP_OK on success
+ */
+static esp_err_t esp_littlefs_init(const esp_vfs_littlefs_conf_t* conf)
+{
+    int index = -1;
+    esp_err_t err = ESP_FAIL;
+    const esp_partition_t* partition = NULL;
+    esp_littlefs_t * efs = NULL;
+
+    if( _efs_lock == NULL ){
+        static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
+        portENTER_CRITICAL(&mux);
+        if( _efs_lock == NULL ){
+            _efs_lock = xSemaphoreCreateMutex();
+            assert(_efs_lock);
+        }
+        portEXIT_CRITICAL(&mux);
+    }
+
+    xSemaphoreTake(_efs_lock, portMAX_DELAY);
+
+    if (esp_littlefs_get_empty(&index) != ESP_OK) {
+        ESP_LOGE(TAG, "max mounted partitions reached");
+        err = ESP_ERR_INVALID_STATE;
+        goto exit;
+    }
+
+    /* Input and Environment Validation */
+    if (esp_littlefs_by_label(conf->partition_label, &index) == ESP_OK) {
+        ESP_LOGE(TAG, "Partition already used");
+        err = ESP_ERR_INVALID_STATE;
+        goto exit;
+    }
+
+	{
+        uint32_t flash_page_size = g_rom_flashchip.page_size;
+        uint32_t log_page_size = CONFIG_LITTLEFS_PAGE_SIZE;
+        if (log_page_size % flash_page_size != 0) {
+            ESP_LOGE(TAG, "LITTLEFS_PAGE_SIZE is not multiple of flash chip page size (%d)",
+                    flash_page_size);
+            err = ESP_ERR_INVALID_ARG;
+            goto exit;
+        }
+    }
+
+    if ( NULL == conf->partition_label ) {
+        ESP_LOGE(TAG, "Partition label must be provided.");
+        err = ESP_ERR_INVALID_ARG;
+        goto exit;
+    }
+
+    partition = esp_partition_find_first(
+            ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY,
+            conf->partition_label);
+
+    if (!partition) {
+        ESP_LOGE(TAG, "partition \"%s\" could not be found", conf->partition_label);
+        err = ESP_ERR_NOT_FOUND;
+        goto exit;
+    }
+
+    if (partition->encrypted) {
+        // TODO: allow encryption; should probably be fine,
+        // just not allowing until tested.
+        ESP_LOGE(TAG, "littlefs can not run on encrypted partition");
+        err = ESP_ERR_INVALID_STATE;
+        goto exit;
+    }
+
+    /* Allocate Context */
+    efs = calloc(1, sizeof(esp_littlefs_t));
+    if (efs == NULL) {
+        ESP_LOGE(TAG, "esp_littlefs could not be malloced");
+        err = ESP_ERR_NO_MEM;
+        goto exit;
+    }
+    efs->partition = partition;
+
+    { /* LittleFS Configuration */
+        efs->cfg.context = efs;
+
+        // block device operations
+        efs->cfg.read  = littlefs_api_read;
+        efs->cfg.prog  = littlefs_api_prog;
+        efs->cfg.erase = littlefs_api_erase;
+        efs->cfg.sync  = littlefs_api_sync;
+
+        // block device configuration
+        efs->cfg.read_size = CONFIG_LITTLEFS_READ_SIZE;
+        efs->cfg.prog_size = CONFIG_LITTLEFS_WRITE_SIZE;
+        efs->cfg.block_size = CONFIG_LITTLEFS_BLOCK_SIZE;; 
+        efs->cfg.block_count = efs->partition->size / efs->cfg.block_size;
+        efs->cfg.cache_size = CONFIG_LITTLEFS_CACHE_SIZE;
+        efs->cfg.lookahead_size = CONFIG_LITTLEFS_LOOKAHEAD_SIZE;
+        efs->cfg.block_cycles = CONFIG_LITTLEFS_BLOCK_CYCLES;
+    }
+
+    efs->lock = xSemaphoreCreateRecursiveMutex();
+    if (efs->lock == NULL) {
+        ESP_LOGE(TAG, "mutex lock could not be created");
+        err = ESP_ERR_NO_MEM;
+        goto exit;
+    }
+
+    efs->fs = calloc(1, sizeof(lfs_t));
+    if (efs->fs == NULL) {
+        ESP_LOGE(TAG, "littlefs could not be malloced");
+        err = ESP_ERR_NO_MEM;
+        goto exit;
+    }
+
+    // Mount and Error Check
+    _efs[index] = efs;
+    if(!conf->dont_mount){
+        int res = lfs_mount(efs->fs, &efs->cfg);
+
+        if (conf->format_if_mount_failed && res != LFS_ERR_OK) {
+            esp_err_t err;
+            ESP_LOGW(TAG, "mount failed, %s (%i). formatting...", esp_littlefs_errno(res), res);
+            err = esp_littlefs_format(efs->partition->label);
+            if(err != ESP_OK) {
+                ESP_LOGE(TAG, "format failed");
+                err = ESP_FAIL;
+                goto exit;
+            }
+            res = lfs_mount(efs->fs, &efs->cfg);
+        }
+        if (res != LFS_ERR_OK) {
+            ESP_LOGE(TAG, "mount failed, %s (%i)", esp_littlefs_errno(res), res);
+            err = ESP_FAIL;
+            goto exit;
+        }
+        efs->cache_size = 4;
+        efs->cache = calloc(sizeof(*efs->cache), efs->cache_size);
+    }
+
+    err = ESP_OK;
+
+exit:
+    if(err != ESP_OK){
+        if( index >= 0 ) {
+            esp_littlefs_free(&_efs[index]);
+        }
+        else{
+            esp_littlefs_free(&efs);
+        }
+    }
+    xSemaphoreGive(_efs_lock);
+    return err;
+}
+
+/**
+ * @brief
+ * @parameter efs file system context
+ */
+static inline int sem_take(esp_littlefs_t *efs) {
+    int res;
+#if LOG_LOCAL_LEVEL >= 4
+    ESP_LOGD(TAG, "------------------------ Sem Taking [%s]", pcTaskGetTaskName(NULL));
+#endif
+    res = xSemaphoreTakeRecursive(efs->lock, portMAX_DELAY);
+#if LOG_LOCAL_LEVEL >= 4
+    ESP_LOGD(TAG, "--------------------->>> Sem Taken [%s]", pcTaskGetTaskName(NULL));
+#endif
+    return res;
+}
+
+/**
+ * @brief
+ * @parameter efs file system context
+ */
+static inline int sem_give(esp_littlefs_t *efs) {
+#if LOG_LOCAL_LEVEL >= 4
+    ESP_LOGD(TAG, "---------------------<<< Sem Give [%s]", pcTaskGetTaskName(NULL));
+#endif
+    return xSemaphoreGiveRecursive(efs->lock);
+}
+
+
+/* We are using a double allocation system here, which an array and a linked list. 
+   The array contains the pointer to the file descriptor (the index in the array is what's returned to the user).
+   The linked list is used for file descriptors.
+   This means that position of nodes in the list must stay consistent:
+   - Allocation is obvious (append to the list from the head, and realloc the pointers array)
+     There is still a O(N) search in the cache for a free position to store
+   - Searching is a O(1) process (good)
+   - Deallocation is more tricky. That is, for example, 
+     if you need to remove node 5 in a 12 nodes list, you'll have to:
+       1) Mark the 5th position as freed (if it's the last position of the array realloc smaller)
+       2) Walk the list until finding the pointer to the node O(N) and scrub the node so the chained list stays consistent
+       3) Deallocate the node 
+*/
+
+/**
+ * @brief Get a file descriptor
+ * @param[in,out] efs       file system context
+ * @param[out]    file      pointer to a file that'll be filled with a file object
+ * @param[in]     path_len  the length of the filepath in bytes (including terminating zero byte)
+ * @return integer file descriptor. Returns -1 if a FD cannot be obtained.
+ * @warning This must be called with lock taken
+ */
+static int esp_littlefs_allocate_fd(esp_littlefs_t *efs, vfs_littlefs_file_t ** file
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+  , const size_t path_len
+#endif
+    )
+{
+    int i = -1;
+
+    assert( efs->fd_count < UINT16_MAX );
+    assert( efs->cache_size < UINT16_MAX );
+
+    /* Make sure there is enough space in the cache to store new fd */
+    if (efs->fd_count + 1 > efs->cache_size) {
+        uint16_t new_size = (uint16_t)MIN(UINT16_MAX, CONFIG_LITTLEFS_FD_CACHE_REALLOC_FACTOR * efs->cache_size);
+        /* Resize the cache */
+        vfs_littlefs_file_t ** new_cache = realloc(efs->cache, new_size * sizeof(*efs->cache));
+        if (!new_cache) {
+            ESP_LOGE(TAG, "Unable to allocate file cache");
+            return -1; /* If it fails here, no harm is done to the filesystem, so it's safe */
+        }
+        /* Zero out the new portions of the cache */
+        memset(&new_cache[efs->cache_size], 0, (new_size - efs->cache_size) * sizeof(*efs->cache));
+        efs->cache = new_cache;
+        efs->cache_size = new_size;
+    }
+
+
+    /* Allocate file descriptor here now */
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+    *file = calloc(1, sizeof(**file) + path_len);
+#else
+    *file = calloc(1, sizeof(**file));
+#endif
+
+    if (*file == NULL) {
+        /* If it fails here, the file system might have a larger cache, but it's harmless, no need to reverse it */
+        ESP_LOGE(TAG, "Unable to allocate FD");
+        return -1; 
+    }
+
+    /* Starting from here, nothing can fail anymore */
+
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+    /* The trick here is to avoid dual allocation so the path pointer 
+        should point to the next byte after it:
+        file => [ lfs_file | # | next | path | free_space ]
+                                            |  /\
+                                            |__/
+    */
+    (*file)->path = (char*)(*file) + sizeof(**file);
+#endif
+ 
+    /* Now find a free place in cache */
+    for(i=0; i < efs->cache_size; i++) {
+        if (efs->cache[i] == NULL) {
+            efs->cache[i] = *file;
+            break;
+        }
+    }
+    /* Save file in the list */
+    (*file)->next = efs->file;
+    efs->file = *file;
+    efs->fd_count++;
+    return i;
+}
+
+/**
+ * @brief Release a file descriptor
+ * @param[in,out] efs file system context
+ * @param[in] fd File Descriptor to release
+ * @return 0 on success. -1 if a FD cannot be obtained.
+ * @warning This must be called with lock taken
+ */
+static int esp_littlefs_free_fd(esp_littlefs_t *efs, int fd){
+    vfs_littlefs_file_t * file, * head;
+
+    if((uint32_t)fd >= efs->cache_size) {
+        ESP_LOGE(TAG, "FD %d must be <%d.", fd, efs->cache_size);
+        return -1;
+    }
+
+    /* Get the file descriptor to free it */
+    file = efs->cache[fd];
+    head = efs->file;
+    /* Search for file in SLL to remove it */
+    if (file == head) {
+        /* Last file, can't fail */
+        efs->file = efs->file->next;
+    } else {
+        while (head && head->next != file) {
+            head = head->next;
+        }
+        if (!head) {
+            ESP_LOGE(TAG, "Inconsistent list");
+            return -1;
+        }
+        /* Transaction starts here and can't fail anymore */ 
+        head->next = file->next;
+    }
+    efs->cache[fd] = NULL;
+    efs->fd_count--;
+
+    ESP_LOGD(TAG, "Clearing FD");
+    free(file);
+
+#if 0
+    /* Realloc smaller if its possible
+     *     * Find and realloc based on number of trailing NULL ptrs in cache
+     *     * Leave some hysteris to prevent thrashing around resize points
+     * This is disabled for now because it adds unnecessary complexity
+     * and binary size increase that outweights its ebenfits.
+     */
+    if(efs->cache_size > CONFIG_LITTLEFS_FD_CACHE_MIN_SIZE) {
+        uint16_t n_free;
+        uint16_t new_size = efs->cache_size / CONFIG_LITTLEFS_FD_CACHE_REALLOC_FACTOR;
+
+        if(new_size >= CONFIG_LITTLEFS_FD_CACHE_MIN_SIZE) {
+            /* Count number of trailing NULL ptrs */
+            for(n_free=0; n_free < efs->cache_size; n_free++) {
+                if(efs->cache[efs->cache_size - n_free - 1] != NULL) {
+                    break;
+                }
+            }
+
+            if(n_free >= (efs->cache_size - new_size)){
+                new_size += CONFIG_LITTLEFS_FD_CACHE_HYST;
+                ESP_LOGD(TAG, "Reallocating cache %i -> %i", efs->cache_size, new_size);
+                vfs_littlefs_file_t ** new_cache;
+                new_cache = realloc(efs->cache, new_size * sizeof(*efs->cache));
+                /* No harm on realloc failure, continue using the oversized cache */
+                if(new_cache) {
+                    efs->cache = new_cache;
+                    efs->cache_size = new_size;
+                }
+            }
+        }
+    }
+#endif
+
+    return 0;
+}
+
+/**
+ * @brief Compute the 32bit DJB2 hash of the given string.
+ * @param[in]   path the path to hash
+ * @returns the hash for this path 
+ */
+static uint32_t compute_hash(const char * path) {
+    uint32_t hash = 5381;
+    char c;
+
+    while ((c = *path++))
+        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
+    return hash;
+}
+
+/**
+ * @brief finds an open file descriptor by file name.
+ * @param[in,out] efs file system context
+ * @param[in] path File path to check.
+ * @returns integer file descriptor. Returns -1 if not found.
+ * @warning This must be called with lock taken
+ * @warning if CONFIG_LITTLEFS_USE_ONLY_HASH, there is a slim chance an
+ *          erroneous FD may be returned on hash collision.
+ */
+static int esp_littlefs_get_fd_by_name(esp_littlefs_t *efs, const char *path){
+    uint32_t hash = compute_hash(path);
+
+    for(uint16_t i=0, j=0; i < efs->cache_size && j < efs->fd_count; i++){
+        if (efs->cache[i]) {
+            ++j; 
+
+            if (
+                efs->cache[i]->hash == hash  // Faster than strcmp
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+                && strcmp(path, efs->cache[i]->path) == 0  // May as well check incase of hash collision. Usually short-circuited.
+#endif
+            ) {
+                ESP_LOGD(TAG, "Found \"%s\" at FD %d.", path, i);
+                return i;
+            }
+        }
+    }
+    ESP_LOGD(TAG, "Unable to get a find FD for \"%s\"", path);
+    return -1;
+}
+
+/*** Filesystem Hooks ***/
+
+static int vfs_littlefs_open(void* ctx, const char * path, int flags, int mode) {
+    /* Note: mode is currently unused */
+    int fd=-1, lfs_flags, res;
+    esp_littlefs_t *efs = (esp_littlefs_t *)ctx;
+    vfs_littlefs_file_t *file = NULL;
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+    size_t path_len = strlen(path) + 1;  // include NULL terminator
+#endif
+    assert(path);
+
+    ESP_LOGD(TAG, "Opening %s", path);
+
+    /* Convert flags to lfs flags */
+    lfs_flags = esp_littlefs_flags_conv(flags);
+
+    /* Get a FD */
+    sem_take(efs);
+    fd = esp_littlefs_allocate_fd(efs, &file
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+    , path_len
+#endif
+    );
+    if(fd < 0) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "Error obtaining FD");
+        return LFS_ERR_INVAL;
+    }
+    /* Open File */
+    res = lfs_file_open(efs->fs, &file->file, path, lfs_flags);
+
+    if( res < 0 ) {
+        esp_littlefs_free_fd(efs, fd);
+        sem_give(efs);
+        ESP_LOGE(TAG, "Failed to open file. Error %s (%d)",
+                esp_littlefs_errno(res), res);
+        return LFS_ERR_INVAL;
+    }
+
+    file->hash = compute_hash(path);
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+    memcpy(file->path, path, path_len);
+#endif
+
+#if CONFIG_LITTLEFS_USE_MTIME
+    if (!(lfs_flags & LFS_O_RDONLY)) {
+        /* If this is being opened as not read-only */
+        vfs_littlefs_update_mtime(efs, path);
+    }
+#endif
+
+    sem_give(efs);
+    ESP_LOGD(TAG, "Done opening %s", path);
+    return fd;
+}
+
+static ssize_t vfs_littlefs_write(void* ctx, int fd, const void * data, size_t size) {
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    ssize_t res;
+    vfs_littlefs_file_t *file = NULL;
+
+    sem_take(efs);
+    if((uint32_t)fd > efs->cache_size) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "FD %d must be <%d.", fd, efs->cache_size);
+        return LFS_ERR_BADF;
+    }
+    file = efs->cache[fd];
+    res = lfs_file_write(efs->fs, &file->file, data, size);
+    sem_give(efs);
+
+    if(res < 0){
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+        ESP_LOGE(TAG, "Failed to write FD %d; path \"%s\". Error %s (%d)",
+                fd, file->path, esp_littlefs_errno(res), res);
+#else
+        ESP_LOGE(TAG, "Failed to write FD %d. Error %s (%d)",
+                fd, esp_littlefs_errno(res), res);
+#endif
+        return res;
+    }
+
+    return res;
+}
+
+static ssize_t vfs_littlefs_read(void* ctx, int fd, void * dst, size_t size) {
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    ssize_t res;
+    vfs_littlefs_file_t *file = NULL;
+
+
+    sem_take(efs);
+    if((uint32_t)fd > efs->cache_size) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "FD %d must be <%d.", fd, efs->cache_size);
+        return LFS_ERR_BADF;
+    }
+    file = efs->cache[fd];
+    res = lfs_file_read(efs->fs, &file->file, dst, size);
+    sem_give(efs);
+
+    if(res < 0){
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+        ESP_LOGE(TAG, "Failed to read file \"%s\". Error %s (%d)",
+                file->path, esp_littlefs_errno(res), res);
+#else
+        ESP_LOGE(TAG, "Failed to read FD %d. Error %s (%d)",
+                fd, esp_littlefs_errno(res), res);
+#endif
+        return res;
+    }
+
+    return res;
+}
+
+static int vfs_littlefs_close(void* ctx, int fd) {
+    // TODO update mtime on close? SPIFFS doesn't do this
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    int res;
+    vfs_littlefs_file_t *file = NULL;
+
+    sem_take(efs);
+    if((uint32_t)fd > efs->cache_size) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "FD %d must be <%d.", fd, efs->cache_size);
+        return LFS_ERR_BADF;
+    }
+    file = efs->cache[fd];
+    res = lfs_file_close(efs->fs, &file->file);
+    if(res < 0){
+        sem_give(efs);
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+        ESP_LOGE(TAG, "Failed to close file \"%s\". Error %s (%d)",
+                file->path, esp_littlefs_errno(res), res);
+#else
+        ESP_LOGE(TAG, "Failed to close Fd %d. Error %s (%d)",
+                fd, esp_littlefs_errno(res), res);
+#endif
+        return res;
+    }
+    esp_littlefs_free_fd(efs, fd);
+    sem_give(efs);
+    return res;
+}
+
+static off_t vfs_littlefs_lseek(void* ctx, int fd, off_t offset, int mode) {
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    lfs_soff_t res;
+    vfs_littlefs_file_t *file = NULL;
+    int whence;
+
+    switch (mode) {
+        case SEEK_SET: whence = LFS_SEEK_SET; break;
+        case SEEK_CUR: whence = LFS_SEEK_CUR; break;
+        case SEEK_END: whence = LFS_SEEK_END; break;
+        default: 
+            ESP_LOGE(TAG, "Invalid mode");
+            return -1;
+    }
+
+    sem_take(efs);
+    if((uint32_t)fd > efs->cache_size) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "FD %d must be <%d.", fd, efs->cache_size);
+        return LFS_ERR_BADF;
+    }
+    file = efs->cache[fd];
+    res = lfs_file_seek(efs->fs, &file->file, offset, whence);
+    sem_give(efs);
+
+    if(res < 0){
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+        ESP_LOGE(TAG, "Failed to seek file \"%s\" to offset %08x. Error %s (%d)",
+                file->path, (unsigned int)offset, esp_littlefs_errno(res), res);
+#else
+        ESP_LOGE(TAG, "Failed to seek FD %d to offset %08x. Error (%d)",
+                fd, (unsigned int)offset, res);
+#endif
+        return res;
+    }
+
+    return res;
+}
+
+static int vfs_littlefs_fsync(void* ctx, int fd)
+{
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    ssize_t res;
+    vfs_littlefs_file_t *file = NULL;
+
+
+    sem_take(efs);
+    if((uint32_t)fd > efs->cache_size) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "FD %d must be <%d.", fd, efs->cache_size);
+        return LFS_ERR_BADF;
+    }
+    file = efs->cache[fd];
+    res = lfs_file_sync(efs->fs, &file->file);
+    sem_give(efs);
+
+    if(res < 0){
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+        ESP_LOGE(TAG, "Failed to sync file \"%s\". Error %s (%d)",
+                file->path, esp_littlefs_errno(res), res);
+#else
+        ESP_LOGE(TAG, "Failed to sync file %d. Error %d", fd, res);
+#endif
+        return res;
+    }
+
+    return res;
+}
+
+
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+static int vfs_littlefs_fstat(void* ctx, int fd, struct stat * st) {
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    struct lfs_info info;
+    int res;
+    vfs_littlefs_file_t *file = NULL;
+
+    memset(st, 0, sizeof(struct stat));
+    st->st_blksize = efs->cfg.block_size;
+
+    sem_take(efs);
+    if((uint32_t)fd > efs->cache_size) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "FD must be <%d.", efs->cache_size);
+        return LFS_ERR_BADF;
+    }
+    file = efs->cache[fd];
+    res = lfs_stat(efs->fs, file->path, &info);
+    if (res < 0) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "Failed to stat file \"%s\". Error %s (%d)",
+                file->path, esp_littlefs_errno(res), res);
+        return res;
+    }
+
+#if CONFIG_LITTLEFS_USE_MTIME  
+    st->st_mtime = vfs_littlefs_get_mtime(efs, file->path);
+#endif
+
+    sem_give(efs);
+
+    st->st_size = info.size;
+    st->st_mode = ((info.type==LFS_TYPE_REG)?S_IFREG:S_IFDIR);
+    return 0;
+}
+#endif
+
+static int vfs_littlefs_stat(void* ctx, const char * path, struct stat * st) {
+    assert(path);
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    struct lfs_info info;
+    int res;
+
+    memset(st, 0, sizeof(struct stat));
+    st->st_blksize = efs->cfg.block_size;
+
+    sem_take(efs);
+    res = lfs_stat(efs->fs, path, &info);
+    if (res < 0) {
+        sem_give(efs);
+        /* Not strictly an error, since stat can be used to check
+         * if a file exists */
+        ESP_LOGI(TAG, "Failed to stat path \"%s\". Error %s (%d)",
+                path, esp_littlefs_errno(res), res);
+        return res;
+    }
+#if CONFIG_LITTLEFS_USE_MTIME    
+    st->st_mtime = vfs_littlefs_get_mtime(efs, path);
+#endif
+    sem_give(efs);
+    st->st_size = info.size;
+    st->st_mode = ((info.type==LFS_TYPE_REG)?S_IFREG:S_IFDIR);
+    return 0;
+}
+
+static int vfs_littlefs_unlink(void* ctx, const char *path) {
+#define fail_str_1 "Failed to unlink path \"%s\"."
+    assert(path);
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    struct lfs_info info;
+    int res;
+
+    sem_take(efs);
+    res = lfs_stat(efs->fs, path, &info);
+    if (res < 0) {
+        sem_give(efs);
+        ESP_LOGE(TAG, fail_str_1 " Error %s (%d)",
+                path, esp_littlefs_errno(res), res);
+        return res;
+    }
+
+    if(esp_littlefs_get_fd_by_name(efs, path) >= 0) {
+        sem_give(efs);
+        ESP_LOGE(TAG, fail_str_1 " Has open FD.", path);
+        return -1;
+    }
+
+    if (info.type == LFS_TYPE_DIR) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "Cannot unlink a directory.");
+        return LFS_ERR_ISDIR;
+    }
+
+    res = lfs_remove(efs->fs, path);
+    if (res < 0) {
+        sem_give(efs);
+        ESP_LOGE(TAG, fail_str_1 " Error %s (%d)",
+                path, esp_littlefs_errno(res), res);
+        return res;
+    }
+
+    sem_give(efs);
+
+    return 0;
+#undef fail_str_1
+}
+
+static int vfs_littlefs_rename(void* ctx, const char *src, const char *dst) {
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    int res;
+
+    sem_take(efs);
+
+    if(esp_littlefs_get_fd_by_name(efs, src) >= 0){
+        sem_give(efs);
+        ESP_LOGE(TAG, "Cannot rename; src \"%s\" is open.", src);
+        return -1;
+    }
+    else if(esp_littlefs_get_fd_by_name(efs, dst) >= 0){
+        sem_give(efs);
+        ESP_LOGE(TAG, "Cannot rename; dst \"%s\" is open.", dst);
+        return -1;
+    }
+
+    res = lfs_rename(efs->fs, src, dst);
+    sem_give(efs);
+    if (res < 0) {
+        ESP_LOGE(TAG, "Failed to rename \"%s\" -> \"%s\". Error %s (%d)",
+                src, dst, esp_littlefs_errno(res), res);
+        return res;
+    }
+
+    return 0;
+}
+
+static DIR* vfs_littlefs_opendir(void* ctx, const char* name) {
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    int res;
+    vfs_littlefs_dir_t *dir = NULL;
+
+    dir = calloc(1, sizeof(vfs_littlefs_dir_t));
+    if( dir == NULL ) {
+        ESP_LOGE(TAG, "dir struct could not be malloced");
+        goto exit;
+    }
+
+    dir->path = strdup(name);
+    if(dir->path == NULL){
+        ESP_LOGE(TAG, "dir path name could not be malloced");
+        goto exit;
+    }
+
+    sem_take(efs);
+    res = lfs_dir_open(efs->fs, &dir->d, dir->path);
+    sem_give(efs);
+    if (res < 0) {
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH        
+        ESP_LOGE(TAG, "Failed to opendir \"%s\". Error %s (%d)",
+                dir->path, esp_littlefs_errno(res), res);
+#else
+        ESP_LOGE(TAG, "Failed to opendir \"%s\". Error %d", dir->path, res);
+#endif
+        goto exit;
+    }
+
+    return (DIR *)dir;
+
+exit:
+    esp_littlefs_dir_free(dir);
+    return NULL;
+}
+
+static int vfs_littlefs_closedir(void* ctx, DIR* pdir) {
+    assert(pdir);
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    vfs_littlefs_dir_t * dir = (vfs_littlefs_dir_t *) pdir;
+    int res;
+
+    sem_take(efs);
+    res = lfs_dir_close(efs->fs, &dir->d);
+    sem_give(efs);
+    if (res < 0) {
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH        
+        ESP_LOGE(TAG, "Failed to closedir \"%s\". Error %s (%d)",
+                dir->path, esp_littlefs_errno(res), res);
+#else
+        ESP_LOGE(TAG, "Failed to closedir \"%s\". Error %d", dir->path, res);
+#endif
+        return res;
+    }
+
+    esp_littlefs_dir_free(dir);
+    return 0;
+}
+
+static struct dirent* vfs_littlefs_readdir(void* ctx, DIR* pdir) {
+    assert(pdir);
+    vfs_littlefs_dir_t * dir = (vfs_littlefs_dir_t *) pdir;
+    int res;
+    struct dirent* out_dirent;
+
+    res = vfs_littlefs_readdir_r(ctx, pdir, &dir->e, &out_dirent);
+    if (res != 0) return NULL;
+    return out_dirent;
+}
+
+static int vfs_littlefs_readdir_r(void* ctx, DIR* pdir,
+        struct dirent* entry, struct dirent** out_dirent) {
+    assert(pdir);
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    vfs_littlefs_dir_t * dir = (vfs_littlefs_dir_t *) pdir;
+    int res;
+    struct lfs_info info = { 0 };
+
+    sem_take(efs);
+    do{ /* Read until we get a real object name */
+        res = lfs_dir_read(efs->fs, &dir->d, &info);
+    }while( res>0 && (strcmp(info.name, ".") == 0 || strcmp(info.name, "..") == 0));
+    sem_give(efs);
+    if (res < 0) {
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH 
+        ESP_LOGE(TAG, "Failed to readdir \"%s\". Error %s (%d)",
+                dir->path, esp_littlefs_errno(res), res);
+#else
+        ESP_LOGE(TAG, "Failed to readdir \"%s\". Error %d", dir->path, res);
+#endif
+        return -1;
+    }
+
+    if(info.type == LFS_TYPE_REG) {
+        ESP_LOGD(TAG, "readdir a file of size %d named \"%s\"",
+                info.size, info.name);
+    }
+    else {
+        ESP_LOGD(TAG, "readdir a dir named \"%s\"", info.name);
+    }
+
+    if(res == 0) {
+        /* End of Objs */
+        ESP_LOGD(TAG, "Reached the end of the directory.");
+        *out_dirent = NULL;
+    }
+    else {
+        entry->d_ino = 0;
+        entry->d_type = info.type == LFS_TYPE_REG ? DT_REG : DT_DIR;
+        strncpy(entry->d_name, info.name, sizeof(entry->d_name));
+        *out_dirent = entry;
+    }
+    dir->offset++;
+
+    return 0;
+}
+
+static long vfs_littlefs_telldir(void* ctx, DIR* pdir) {
+    assert(pdir);
+    vfs_littlefs_dir_t * dir = (vfs_littlefs_dir_t *) pdir;
+    return dir->offset;
+}
+
+static void vfs_littlefs_seekdir(void* ctx, DIR* pdir, long offset) {
+    assert(pdir);
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    vfs_littlefs_dir_t * dir = (vfs_littlefs_dir_t *) pdir;
+    int res;
+
+    if (offset < dir->offset) {
+        /* close and re-open dir to rewind to beginning */
+        sem_take(efs);
+        res = lfs_dir_rewind(efs->fs, &dir->d);
+        sem_give(efs);
+        if (res < 0) {
+            ESP_LOGE(TAG, "Failed to rewind dir \"%s\". Error %s (%d)",
+                    dir->path, esp_littlefs_errno(res), res);
+            return;
+        }
+        dir->offset = 0;
+    }
+
+    while(dir->offset < offset){
+        struct dirent *out_dirent;
+        res = vfs_littlefs_readdir_r(ctx, pdir, &dir->e, &out_dirent);
+        if( res != 0 ){
+            ESP_LOGE(TAG, "Error readdir_r");
+            return;
+        }
+    }
+}
+
+static int vfs_littlefs_mkdir(void* ctx, const char* name, mode_t mode) {
+    /* Note: mode is currently unused */
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    int res;
+    ESP_LOGD(TAG, "mkdir \"%s\"", name);
+
+    sem_take(efs);
+    res = lfs_mkdir(efs->fs, name);
+    sem_give(efs);
+    if (res < 0) {
+        ESP_LOGE(TAG, "Failed to mkdir \"%s\". Error %s (%d)",
+                name, esp_littlefs_errno(res), res);
+        return res;
+    }
+    return 0;
+}
+
+static int vfs_littlefs_rmdir(void* ctx, const char* name) {
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    struct lfs_info info;
+    int res;
+
+    /* Error Checking */
+    sem_take(efs);
+    res = lfs_stat(efs->fs, name, &info);
+    if (res < 0) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "\"%s\" doesn't exist.", name);
+        return -1;
+    }
+
+    if (info.type != LFS_TYPE_DIR) {
+        sem_give(efs);
+        ESP_LOGE(TAG, "\"%s\" is not a directory.", name);
+        return -1;
+    }
+
+    /* Unlink the dir */
+    res = lfs_remove(efs->fs, name);
+    sem_give(efs);
+    if ( res < 0) {
+        ESP_LOGE(TAG, "Failed to unlink path \"%s\". Error %s (%d)",
+                name, esp_littlefs_errno(res), res);
+        return -1;
+    }
+
+    return 0;
+}
+
+#if CONFIG_LITTLEFS_USE_MTIME
+/**
+ * Sets the mtime attr to t.
+ */
+static int vfs_littlefs_update_mtime_value(esp_littlefs_t *efs, const char *path, time_t t)
+{
+    int res;
+    res = lfs_setattr(efs->fs, path, LITTLEFS_ATTR_MTIME,
+            &t, sizeof(t));
+    if( res < 0 ) {
+        ESP_LOGE(TAG, "Failed to update mtime (%d)", res);
+    }
+
+    return res;
+}
+
+/**
+ * Sets the mtime attr to an appropriate value
+ */
+static void vfs_littlefs_update_mtime(esp_littlefs_t *efs, const char *path)
+{
+    vfs_littlefs_utime(efs, path, NULL);
+}
+
+
+static int vfs_littlefs_utime(void *ctx, const char *path, const struct utimbuf *times)
+{
+    esp_littlefs_t * efs = (esp_littlefs_t *)ctx;
+    time_t t;
+
+    assert(path);
+
+    if (times) {
+        t = times->modtime;
+    } else {
+#if CONFIG_LITTLEFS_MTIME_USE_SECONDS
+        // use current time
+        t = time(NULL);
+#elif CONFIG_LITTLEFS_MTIME_USE_NONCE
+        assert( sizeof(time_t) == 4 );
+        t = vfs_littlefs_get_mtime(efs, path);
+        if( 0 == t ) t = esp_random();
+        else t += 1;
+
+        if( 0 == t ) t = 1;
+#else
+#error "Invalid MTIME configuration"
+#endif
+    }
+
+    int ret = vfs_littlefs_update_mtime_value(efs, path, t);
+    return ret;
+}
+
+static time_t vfs_littlefs_get_mtime(esp_littlefs_t *efs, const char *path)
+{
+    time_t t = 0;
+    int size;
+    size = lfs_getattr(efs->fs, path, LITTLEFS_ATTR_MTIME,
+            &t, sizeof(t));
+    if( size < 0 ) {
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH        
+        ESP_LOGI(TAG, "Failed to get mtime attribute %s (%d)",
+                esp_littlefs_errno(size), size);
+#else
+        ESP_LOGI(TAG, "Failed to get mtime attribute %d", size);
+#endif
+    }
+    return t;
+}
+#endif //CONFIG_LITTLEFS_USE_MTIME

+ 114 - 0
src/esp_littlefs.h

@@ -0,0 +1,114 @@
+#ifndef ESP_LITTLEFS_H__
+#define ESP_LITTLEFS_H__
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <utime.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
+#include "esp_err.h"
+#include <sys/types.h>
+#include <sys/reent.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/termios.h>
+#include <sys/poll.h>
+#include <dirent.h>
+#include <string.h>
+#include "sdkconfig.h"
+
+#include "lfs.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum {
+    LITTLEFS_ATTR_MTIME,   /**< Last Modified - time (seconds) */
+    LITTLEFS_ATTR_MAX
+};
+
+/**
+ *Configuration structure for esp_vfs_littlefs_register.
+ */
+typedef struct {
+    const char *base_path;            /**< Mounting point. */
+    const char *partition_label;      /**< Label of partition to use. */
+    uint8_t format_if_mount_failed:1; /**< Format the file system if it fails to mount. */
+    uint8_t dont_mount:1;             /**< Don't attempt to mount or format. Overrides format_if_mount_failed */
+} esp_vfs_littlefs_conf_t;
+
+/**
+ * Register and mount littlefs to VFS with given path prefix.
+ *
+ * @param   conf                      Pointer to esp_vfs_littlefs_conf_t configuration structure
+ *
+ * @return  
+ *          - ESP_OK                  if success
+ *          - ESP_ERR_NO_MEM          if objects could not be allocated
+ *          - ESP_ERR_INVALID_STATE   if already mounted or partition is encrypted
+ *          - ESP_ERR_NOT_FOUND       if partition for littlefs was not found
+ *          - ESP_FAIL                if mount or format fails
+ */
+esp_err_t esp_vfs_littlefs_register(const esp_vfs_littlefs_conf_t * conf);
+
+/**
+ * Unregister and unmount littlefs from VFS
+ *
+ * @param partition_label  Label of the partition to unregister.
+ *
+ * @return  
+ *          - ESP_OK if successful
+ *          - ESP_ERR_INVALID_STATE already unregistered
+ */
+esp_err_t esp_vfs_littlefs_unregister(const char* partition_label);
+
+/**
+ * Check if littlefs is mounted
+ *
+ * @param partition_label  Label of the partition to check.
+ *
+ * @return  
+ *          - true    if mounted
+ *          - false   if not mounted
+ */
+bool esp_littlefs_mounted(const char* partition_label);
+
+/**
+ * Format the littlefs partition
+ *
+ * @param partition_label  Label of the partition to format.
+ * @return  
+ *          - ESP_OK      if successful
+ *          - ESP_FAIL    on error
+ */
+esp_err_t esp_littlefs_format(const char* partition_label);
+
+/**
+ * Get information for littlefs
+ *
+ * @param partition_label           Optional, label of the partition to get info for.
+ * @param[out] total_bytes          Size of the file system
+ * @param[out] used_bytes           Current used bytes in the file system
+ *
+ * @return  
+ *          - ESP_OK                  if success
+ *          - ESP_ERR_INVALID_STATE   if not mounted
+ */
+esp_err_t esp_littlefs_info(const char* partition_label, size_t *total_bytes, size_t *used_bytes);
+
+#if CONFIG_LITTLEFS_HUMAN_READABLE
+/**
+ * @brief converts an enumerated lfs error into a string.
+ * @param lfs_errno The enumerated littlefs error.
+ */
+const char * esp_littlefs_errno(enum lfs_error lfs_errno);
+#endif
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif

+ 4913 - 0
src/lfs.c

@@ -0,0 +1,4913 @@
+/*
+ * The little filesystem
+ *
+ * Copyright (c) 2017, Arm Limited. All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#include "lfs.h"
+#include "lfs_util.h"
+
+#define LFS_BLOCK_NULL ((lfs_block_t)-1)
+#define LFS_BLOCK_INLINE ((lfs_block_t)-2)
+
+/// Caching block device operations ///
+static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) {
+    // do not zero, cheaper if cache is readonly or only going to be
+    // written with identical data (during relocates)
+    (void)lfs;
+    rcache->block = LFS_BLOCK_NULL;
+}
+
+static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) {
+    // zero to avoid information leak
+    memset(pcache->buffer, 0xff, lfs->cfg->cache_size);
+    pcache->block = LFS_BLOCK_NULL;
+}
+
+static int lfs_bd_read(lfs_t *lfs,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint,
+        lfs_block_t block, lfs_off_t off,
+        void *buffer, lfs_size_t size) {
+    uint8_t *data = buffer;
+    if (block >= lfs->cfg->block_count ||
+            off+size > lfs->cfg->block_size) {
+        return LFS_ERR_CORRUPT;
+    }
+
+    while (size > 0) {
+        lfs_size_t diff = size;
+
+        if (pcache && block == pcache->block &&
+                off < pcache->off + pcache->size) {
+            if (off >= pcache->off) {
+                // is already in pcache?
+                diff = lfs_min(diff, pcache->size - (off-pcache->off));
+                memcpy(data, &pcache->buffer[off-pcache->off], diff);
+
+                data += diff;
+                off += diff;
+                size -= diff;
+                continue;
+            }
+
+            // pcache takes priority
+            diff = lfs_min(diff, pcache->off-off);
+        }
+
+        if (block == rcache->block &&
+                off < rcache->off + rcache->size) {
+            if (off >= rcache->off) {
+                // is already in rcache?
+                diff = lfs_min(diff, rcache->size - (off-rcache->off));
+                memcpy(data, &rcache->buffer[off-rcache->off], diff);
+
+                data += diff;
+                off += diff;
+                size -= diff;
+                continue;
+            }
+
+            // rcache takes priority
+            diff = lfs_min(diff, rcache->off-off);
+        }
+
+        if (size >= hint && off % lfs->cfg->read_size == 0 &&
+                size >= lfs->cfg->read_size) {
+            // bypass cache?
+            diff = lfs_aligndown(diff, lfs->cfg->read_size);
+            int err = lfs->cfg->read(lfs->cfg, block, off, data, diff);
+            if (err) {
+                return err;
+            }
+
+            data += diff;
+            off += diff;
+            size -= diff;
+            continue;
+        }
+
+        // load to cache, first condition can no longer fail
+        LFS_ASSERT(block < lfs->cfg->block_count);
+        rcache->block = block;
+        rcache->off = lfs_aligndown(off, lfs->cfg->read_size);
+        rcache->size = lfs_min(
+                lfs_min(
+                    lfs_alignup(off+hint, lfs->cfg->read_size),
+                    lfs->cfg->block_size)
+                - rcache->off,
+                lfs->cfg->cache_size);
+        int err = lfs->cfg->read(lfs->cfg, rcache->block,
+                rcache->off, rcache->buffer, rcache->size);
+        LFS_ASSERT(err <= 0);
+        if (err) {
+            return err;
+        }
+    }
+
+    return 0;
+}
+
+enum {
+    LFS_CMP_EQ = 0,
+    LFS_CMP_LT = 1,
+    LFS_CMP_GT = 2,
+};
+
+static int lfs_bd_cmp(lfs_t *lfs,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint,
+        lfs_block_t block, lfs_off_t off,
+        const void *buffer, lfs_size_t size) {
+    const uint8_t *data = buffer;
+
+    for (lfs_off_t i = 0; i < size; i++) {
+        uint8_t dat;
+        int err = lfs_bd_read(lfs,
+                pcache, rcache, hint-i,
+                block, off+i, &dat, 1);
+        if (err) {
+            return err;
+        }
+
+        if (dat != data[i]) {
+            return (dat < data[i]) ? LFS_CMP_LT : LFS_CMP_GT;
+        }
+    }
+
+    return LFS_CMP_EQ;
+}
+
+static int lfs_bd_flush(lfs_t *lfs,
+        lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) {
+    if (pcache->block != LFS_BLOCK_NULL && pcache->block != LFS_BLOCK_INLINE) {
+        LFS_ASSERT(pcache->block < lfs->cfg->block_count);
+        lfs_size_t diff = lfs_alignup(pcache->size, lfs->cfg->prog_size);
+        int err = lfs->cfg->prog(lfs->cfg, pcache->block,
+                pcache->off, pcache->buffer, diff);
+        LFS_ASSERT(err <= 0);
+        if (err) {
+            return err;
+        }
+
+        if (validate) {
+            // check data on disk
+            lfs_cache_drop(lfs, rcache);
+            int res = lfs_bd_cmp(lfs,
+                    NULL, rcache, diff,
+                    pcache->block, pcache->off, pcache->buffer, diff);
+            if (res < 0) {
+                return res;
+            }
+
+            if (res != LFS_CMP_EQ) {
+                return LFS_ERR_CORRUPT;
+            }
+        }
+
+        lfs_cache_zero(lfs, pcache);
+    }
+
+    return 0;
+}
+
+static int lfs_bd_sync(lfs_t *lfs,
+        lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) {
+    lfs_cache_drop(lfs, rcache);
+
+    int err = lfs_bd_flush(lfs, pcache, rcache, validate);
+    if (err) {
+        return err;
+    }
+
+    err = lfs->cfg->sync(lfs->cfg);
+    LFS_ASSERT(err <= 0);
+    return err;
+}
+
+static int lfs_bd_prog(lfs_t *lfs,
+        lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate,
+        lfs_block_t block, lfs_off_t off,
+        const void *buffer, lfs_size_t size) {
+    const uint8_t *data = buffer;
+    LFS_ASSERT(block == LFS_BLOCK_INLINE || block < lfs->cfg->block_count);
+    LFS_ASSERT(off + size <= lfs->cfg->block_size);
+
+    while (size > 0) {
+        if (block == pcache->block &&
+                off >= pcache->off &&
+                off < pcache->off + lfs->cfg->cache_size) {
+            // already fits in pcache?
+            lfs_size_t diff = lfs_min(size,
+                    lfs->cfg->cache_size - (off-pcache->off));
+            memcpy(&pcache->buffer[off-pcache->off], data, diff);
+
+            data += diff;
+            off += diff;
+            size -= diff;
+
+            pcache->size = lfs_max(pcache->size, off - pcache->off);
+            if (pcache->size == lfs->cfg->cache_size) {
+                // eagerly flush out pcache if we fill up
+                int err = lfs_bd_flush(lfs, pcache, rcache, validate);
+                if (err) {
+                    return err;
+                }
+            }
+
+            continue;
+        }
+
+        // pcache must have been flushed, either by programming and
+        // entire block or manually flushing the pcache
+        LFS_ASSERT(pcache->block == LFS_BLOCK_NULL);
+
+        // prepare pcache, first condition can no longer fail
+        pcache->block = block;
+        pcache->off = lfs_aligndown(off, lfs->cfg->prog_size);
+        pcache->size = 0;
+    }
+
+    return 0;
+}
+
+static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) {
+    LFS_ASSERT(block < lfs->cfg->block_count);
+    int err = lfs->cfg->erase(lfs->cfg, block);
+    LFS_ASSERT(err <= 0);
+    return err;
+}
+
+
+/// Small type-level utilities ///
+// operations on block pairs
+static inline void lfs_pair_swap(lfs_block_t pair[2]) {
+    lfs_block_t t = pair[0];
+    pair[0] = pair[1];
+    pair[1] = t;
+}
+
+static inline bool lfs_pair_isnull(const lfs_block_t pair[2]) {
+    return pair[0] == LFS_BLOCK_NULL || pair[1] == LFS_BLOCK_NULL;
+}
+
+static inline int lfs_pair_cmp(
+        const lfs_block_t paira[2],
+        const lfs_block_t pairb[2]) {
+    return !(paira[0] == pairb[0] || paira[1] == pairb[1] ||
+             paira[0] == pairb[1] || paira[1] == pairb[0]);
+}
+
+static inline bool lfs_pair_sync(
+        const lfs_block_t paira[2],
+        const lfs_block_t pairb[2]) {
+    return (paira[0] == pairb[0] && paira[1] == pairb[1]) ||
+           (paira[0] == pairb[1] && paira[1] == pairb[0]);
+}
+
+static inline void lfs_pair_fromle32(lfs_block_t pair[2]) {
+    pair[0] = lfs_fromle32(pair[0]);
+    pair[1] = lfs_fromle32(pair[1]);
+}
+
+static inline void lfs_pair_tole32(lfs_block_t pair[2]) {
+    pair[0] = lfs_tole32(pair[0]);
+    pair[1] = lfs_tole32(pair[1]);
+}
+
+// operations on 32-bit entry tags
+typedef uint32_t lfs_tag_t;
+typedef int32_t lfs_stag_t;
+
+#define LFS_MKTAG(type, id, size) \
+    (((lfs_tag_t)(type) << 20) | ((lfs_tag_t)(id) << 10) | (lfs_tag_t)(size))
+
+#define LFS_MKTAG_IF(cond, type, id, size) \
+    ((cond) ? LFS_MKTAG(type, id, size) : LFS_MKTAG(LFS_FROM_NOOP, 0, 0))
+
+#define LFS_MKTAG_IF_ELSE(cond, type1, id1, size1, type2, id2, size2) \
+    ((cond) ? LFS_MKTAG(type1, id1, size1) : LFS_MKTAG(type2, id2, size2))
+
+static inline bool lfs_tag_isvalid(lfs_tag_t tag) {
+    return !(tag & 0x80000000);
+}
+
+static inline bool lfs_tag_isdelete(lfs_tag_t tag) {
+    return ((int32_t)(tag << 22) >> 22) == -1;
+}
+
+static inline uint16_t lfs_tag_type1(lfs_tag_t tag) {
+    return (tag & 0x70000000) >> 20;
+}
+
+static inline uint16_t lfs_tag_type3(lfs_tag_t tag) {
+    return (tag & 0x7ff00000) >> 20;
+}
+
+static inline uint8_t lfs_tag_chunk(lfs_tag_t tag) {
+    return (tag & 0x0ff00000) >> 20;
+}
+
+static inline int8_t lfs_tag_splice(lfs_tag_t tag) {
+    return (int8_t)lfs_tag_chunk(tag);
+}
+
+static inline uint16_t lfs_tag_id(lfs_tag_t tag) {
+    return (tag & 0x000ffc00) >> 10;
+}
+
+static inline lfs_size_t lfs_tag_size(lfs_tag_t tag) {
+    return tag & 0x000003ff;
+}
+
+static inline lfs_size_t lfs_tag_dsize(lfs_tag_t tag) {
+    return sizeof(tag) + lfs_tag_size(tag + lfs_tag_isdelete(tag));
+}
+
+// operations on attributes in attribute lists
+struct lfs_mattr {
+    lfs_tag_t tag;
+    const void *buffer;
+};
+
+struct lfs_diskoff {
+    lfs_block_t block;
+    lfs_off_t off;
+};
+
+#define LFS_MKATTRS(...) \
+    (struct lfs_mattr[]){__VA_ARGS__}, \
+    sizeof((struct lfs_mattr[]){__VA_ARGS__}) / sizeof(struct lfs_mattr)
+
+// operations on global state
+static inline void lfs_gstate_xor(lfs_gstate_t *a, const lfs_gstate_t *b) {
+    for (int i = 0; i < 3; i++) {
+        ((uint32_t*)a)[i] ^= ((const uint32_t*)b)[i];
+    }
+}
+
+static inline bool lfs_gstate_iszero(const lfs_gstate_t *a) {
+    for (int i = 0; i < 3; i++) {
+        if (((uint32_t*)a)[i] != 0) {
+            return false;
+        }
+    }
+    return true;
+}
+
+static inline bool lfs_gstate_hasorphans(const lfs_gstate_t *a) {
+    return lfs_tag_size(a->tag);
+}
+
+static inline uint8_t lfs_gstate_getorphans(const lfs_gstate_t *a) {
+    return lfs_tag_size(a->tag);
+}
+
+static inline bool lfs_gstate_hasmove(const lfs_gstate_t *a) {
+    return lfs_tag_type1(a->tag);
+}
+
+static inline bool lfs_gstate_hasmovehere(const lfs_gstate_t *a,
+        const lfs_block_t *pair) {
+    return lfs_tag_type1(a->tag) && lfs_pair_cmp(a->pair, pair) == 0;
+}
+
+static inline void lfs_gstate_fromle32(lfs_gstate_t *a) {
+    a->tag     = lfs_fromle32(a->tag);
+    a->pair[0] = lfs_fromle32(a->pair[0]);
+    a->pair[1] = lfs_fromle32(a->pair[1]);
+}
+
+static inline void lfs_gstate_tole32(lfs_gstate_t *a) {
+    a->tag     = lfs_tole32(a->tag);
+    a->pair[0] = lfs_tole32(a->pair[0]);
+    a->pair[1] = lfs_tole32(a->pair[1]);
+}
+
+// other endianness operations
+static void lfs_ctz_fromle32(struct lfs_ctz *ctz) {
+    ctz->head = lfs_fromle32(ctz->head);
+    ctz->size = lfs_fromle32(ctz->size);
+}
+
+static void lfs_ctz_tole32(struct lfs_ctz *ctz) {
+    ctz->head = lfs_tole32(ctz->head);
+    ctz->size = lfs_tole32(ctz->size);
+}
+
+static inline void lfs_superblock_fromle32(lfs_superblock_t *superblock) {
+    superblock->version     = lfs_fromle32(superblock->version);
+    superblock->block_size  = lfs_fromle32(superblock->block_size);
+    superblock->block_count = lfs_fromle32(superblock->block_count);
+    superblock->name_max    = lfs_fromle32(superblock->name_max);
+    superblock->file_max    = lfs_fromle32(superblock->file_max);
+    superblock->attr_max    = lfs_fromle32(superblock->attr_max);
+}
+
+static inline void lfs_superblock_tole32(lfs_superblock_t *superblock) {
+    superblock->version     = lfs_tole32(superblock->version);
+    superblock->block_size  = lfs_tole32(superblock->block_size);
+    superblock->block_count = lfs_tole32(superblock->block_count);
+    superblock->name_max    = lfs_tole32(superblock->name_max);
+    superblock->file_max    = lfs_tole32(superblock->file_max);
+    superblock->attr_max    = lfs_tole32(superblock->attr_max);
+}
+
+
+/// Internal operations predeclared here ///
+static int lfs_dir_commit(lfs_t *lfs, lfs_mdir_t *dir,
+        const struct lfs_mattr *attrs, int attrcount);
+static int lfs_dir_compact(lfs_t *lfs,
+        lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount,
+        lfs_mdir_t *source, uint16_t begin, uint16_t end);
+static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file);
+static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file);
+static void lfs_fs_preporphans(lfs_t *lfs, int8_t orphans);
+static void lfs_fs_prepmove(lfs_t *lfs,
+        uint16_t id, const lfs_block_t pair[2]);
+static int lfs_fs_pred(lfs_t *lfs, const lfs_block_t dir[2],
+        lfs_mdir_t *pdir);
+static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t dir[2],
+        lfs_mdir_t *parent);
+static int lfs_fs_relocate(lfs_t *lfs,
+        const lfs_block_t oldpair[2], lfs_block_t newpair[2]);
+int lfs_fs_traverseraw(lfs_t *lfs,
+        int (*cb)(void *data, lfs_block_t block), void *data,
+        bool includeorphans);
+static int lfs_fs_forceconsistency(lfs_t *lfs);
+static int lfs_deinit(lfs_t *lfs);
+#ifdef LFS_MIGRATE
+static int lfs1_traverse(lfs_t *lfs,
+        int (*cb)(void*, lfs_block_t), void *data);
+#endif
+
+/// Block allocator ///
+static int lfs_alloc_lookahead(void *p, lfs_block_t block) {
+    lfs_t *lfs = (lfs_t*)p;
+    lfs_block_t off = ((block - lfs->free.off)
+            + lfs->cfg->block_count) % lfs->cfg->block_count;
+
+    if (off < lfs->free.size) {
+        lfs->free.buffer[off / 32] |= 1U << (off % 32);
+    }
+
+    return 0;
+}
+
+static void lfs_alloc_ack(lfs_t *lfs) {
+    lfs->free.ack = lfs->cfg->block_count;
+}
+
+// Invalidate the lookahead buffer. This is done during mounting and
+// failed traversals
+static void lfs_alloc_reset(lfs_t *lfs) {
+    lfs->free.off = lfs->seed % lfs->cfg->block_size;
+    lfs->free.size = 0;
+    lfs->free.i = 0;
+    lfs_alloc_ack(lfs);
+}
+
+static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) {
+    while (true) {
+        while (lfs->free.i != lfs->free.size) {
+            lfs_block_t off = lfs->free.i;
+            lfs->free.i += 1;
+            lfs->free.ack -= 1;
+
+            if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) {
+                // found a free block
+                *block = (lfs->free.off + off) % lfs->cfg->block_count;
+
+                // eagerly find next off so an alloc ack can
+                // discredit old lookahead blocks
+                while (lfs->free.i != lfs->free.size &&
+                        (lfs->free.buffer[lfs->free.i / 32]
+                            & (1U << (lfs->free.i % 32)))) {
+                    lfs->free.i += 1;
+                    lfs->free.ack -= 1;
+                }
+
+                return 0;
+            }
+        }
+
+        // check if we have looked at all blocks since last ack
+        if (lfs->free.ack == 0) {
+            LFS_ERROR("No more free space %"PRIu32,
+                    lfs->free.i + lfs->free.off);
+            return LFS_ERR_NOSPC;
+        }
+
+        lfs->free.off = (lfs->free.off + lfs->free.size)
+                % lfs->cfg->block_count;
+        lfs->free.size = lfs_min(8*lfs->cfg->lookahead_size, lfs->free.ack);
+        lfs->free.i = 0;
+
+        // find mask of free blocks from tree
+        memset(lfs->free.buffer, 0, lfs->cfg->lookahead_size);
+        int err = lfs_fs_traverseraw(lfs, lfs_alloc_lookahead, lfs, true);
+        if (err) {
+            lfs_alloc_reset(lfs);
+            return err;
+        }
+    }
+}
+
+/// Metadata pair and directory operations ///
+static lfs_stag_t lfs_dir_getslice(lfs_t *lfs, const lfs_mdir_t *dir,
+        lfs_tag_t gmask, lfs_tag_t gtag,
+        lfs_off_t goff, void *gbuffer, lfs_size_t gsize) {
+    lfs_off_t off = dir->off;
+    lfs_tag_t ntag = dir->etag;
+    lfs_stag_t gdiff = 0;
+
+    if (lfs_gstate_hasmovehere(&lfs->gdisk, dir->pair) &&
+            lfs_tag_id(gmask) != 0 &&
+            lfs_tag_id(lfs->gdisk.tag) <= lfs_tag_id(gtag)) {
+        // synthetic moves
+        gdiff -= LFS_MKTAG(0, 1, 0);
+    }
+
+    // iterate over dir block backwards (for faster lookups)
+    while (off >= sizeof(lfs_tag_t) + lfs_tag_dsize(ntag)) {
+        off -= lfs_tag_dsize(ntag);
+        lfs_tag_t tag = ntag;
+        int err = lfs_bd_read(lfs,
+                NULL, &lfs->rcache, sizeof(ntag),
+                dir->pair[0], off, &ntag, sizeof(ntag));
+        if (err) {
+            return err;
+        }
+
+        ntag = (lfs_frombe32(ntag) ^ tag) & 0x7fffffff;
+
+        if (lfs_tag_id(gmask) != 0 &&
+                lfs_tag_type1(tag) == LFS_TYPE_SPLICE &&
+                lfs_tag_id(tag) <= lfs_tag_id(gtag - gdiff)) {
+            if (tag == (LFS_MKTAG(LFS_TYPE_CREATE, 0, 0) |
+                    (LFS_MKTAG(0, 0x3ff, 0) & (gtag - gdiff)))) {
+                // found where we were created
+                return LFS_ERR_NOENT;
+            }
+
+            // move around splices
+            gdiff += LFS_MKTAG(0, lfs_tag_splice(tag), 0);
+        }
+
+        if ((gmask & tag) == (gmask & (gtag - gdiff))) {
+            if (lfs_tag_isdelete(tag)) {
+                return LFS_ERR_NOENT;
+            }
+
+            lfs_size_t diff = lfs_min(lfs_tag_size(tag), gsize);
+            err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, diff,
+                    dir->pair[0], off+sizeof(tag)+goff, gbuffer, diff);
+            if (err) {
+                return err;
+            }
+
+            memset((uint8_t*)gbuffer + diff, 0, gsize - diff);
+
+            return tag + gdiff;
+        }
+    }
+
+    return LFS_ERR_NOENT;
+}
+
+static lfs_stag_t lfs_dir_get(lfs_t *lfs, const lfs_mdir_t *dir,
+        lfs_tag_t gmask, lfs_tag_t gtag, void *buffer) {
+    return lfs_dir_getslice(lfs, dir,
+            gmask, gtag,
+            0, buffer, lfs_tag_size(gtag));
+}
+
+static int lfs_dir_getread(lfs_t *lfs, const lfs_mdir_t *dir,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint,
+        lfs_tag_t gmask, lfs_tag_t gtag,
+        lfs_off_t off, void *buffer, lfs_size_t size) {
+    uint8_t *data = buffer;
+    if (off+size > lfs->cfg->block_size) {
+        return LFS_ERR_CORRUPT;
+    }
+
+    while (size > 0) {
+        lfs_size_t diff = size;
+
+        if (pcache && pcache->block == LFS_BLOCK_INLINE &&
+                off < pcache->off + pcache->size) {
+            if (off >= pcache->off) {
+                // is already in pcache?
+                diff = lfs_min(diff, pcache->size - (off-pcache->off));
+                memcpy(data, &pcache->buffer[off-pcache->off], diff);
+
+                data += diff;
+                off += diff;
+                size -= diff;
+                continue;
+            }
+
+            // pcache takes priority
+            diff = lfs_min(diff, pcache->off-off);
+        }
+
+        if (rcache->block == LFS_BLOCK_INLINE &&
+                off < rcache->off + rcache->size) {
+            if (off >= rcache->off) {
+                // is already in rcache?
+                diff = lfs_min(diff, rcache->size - (off-rcache->off));
+                memcpy(data, &rcache->buffer[off-rcache->off], diff);
+
+                data += diff;
+                off += diff;
+                size -= diff;
+                continue;
+            }
+
+            // rcache takes priority
+            diff = lfs_min(diff, rcache->off-off);
+        }
+
+        // load to cache, first condition can no longer fail
+        rcache->block = LFS_BLOCK_INLINE;
+        rcache->off = lfs_aligndown(off, lfs->cfg->read_size);
+        rcache->size = lfs_min(lfs_alignup(off+hint, lfs->cfg->read_size),
+                lfs->cfg->cache_size);
+        int err = lfs_dir_getslice(lfs, dir, gmask, gtag,
+                rcache->off, rcache->buffer, rcache->size);
+        if (err < 0) {
+            return err;
+        }
+    }
+
+    return 0;
+}
+
+static int lfs_dir_traverse_filter(void *p,
+        lfs_tag_t tag, const void *buffer) {
+    lfs_tag_t *filtertag = p;
+    (void)buffer;
+
+    // which mask depends on unique bit in tag structure
+    uint32_t mask = (tag & LFS_MKTAG(0x100, 0, 0))
+            ? LFS_MKTAG(0x7ff, 0x3ff, 0)
+            : LFS_MKTAG(0x700, 0x3ff, 0);
+
+    // check for redundancy
+    if ((mask & tag) == (mask & *filtertag) ||
+            lfs_tag_isdelete(*filtertag) ||
+            (LFS_MKTAG(0x7ff, 0x3ff, 0) & tag) == (
+                LFS_MKTAG(LFS_TYPE_DELETE, 0, 0) |
+                    (LFS_MKTAG(0, 0x3ff, 0) & *filtertag))) {
+        return true;
+    }
+
+    // check if we need to adjust for created/deleted tags
+    if (lfs_tag_type1(tag) == LFS_TYPE_SPLICE &&
+            lfs_tag_id(tag) <= lfs_tag_id(*filtertag)) {
+        *filtertag += LFS_MKTAG(0, lfs_tag_splice(tag), 0);
+    }
+
+    return false;
+}
+
+static int lfs_dir_traverse(lfs_t *lfs,
+        const lfs_mdir_t *dir, lfs_off_t off, lfs_tag_t ptag,
+        const struct lfs_mattr *attrs, int attrcount,
+        lfs_tag_t tmask, lfs_tag_t ttag,
+        uint16_t begin, uint16_t end, int16_t diff,
+        int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) {
+    // iterate over directory and attrs
+    while (true) {
+        lfs_tag_t tag;
+        const void *buffer;
+        struct lfs_diskoff disk;
+        if (off+lfs_tag_dsize(ptag) < dir->off) {
+            off += lfs_tag_dsize(ptag);
+            int err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, sizeof(tag),
+                    dir->pair[0], off, &tag, sizeof(tag));
+            if (err) {
+                return err;
+            }
+
+            tag = (lfs_frombe32(tag) ^ ptag) | 0x80000000;
+            disk.block = dir->pair[0];
+            disk.off = off+sizeof(lfs_tag_t);
+            buffer = &disk;
+            ptag = tag;
+        } else if (attrcount > 0) {
+            tag = attrs[0].tag;
+            buffer = attrs[0].buffer;
+            attrs += 1;
+            attrcount -= 1;
+        } else {
+            return 0;
+        }
+
+        lfs_tag_t mask = LFS_MKTAG(0x7ff, 0, 0);
+        if ((mask & tmask & tag) != (mask & tmask & ttag)) {
+            continue;
+        }
+
+        // do we need to filter? inlining the filtering logic here allows
+        // for some minor optimizations
+        if (lfs_tag_id(tmask) != 0) {
+            // scan for duplicates and update tag based on creates/deletes
+            int filter = lfs_dir_traverse(lfs,
+                    dir, off, ptag, attrs, attrcount,
+                    0, 0, 0, 0, 0,
+                    lfs_dir_traverse_filter, &tag);
+            if (filter < 0) {
+                return filter;
+            }
+
+            if (filter) {
+                continue;
+            }
+
+            // in filter range?
+            if (!(lfs_tag_id(tag) >= begin && lfs_tag_id(tag) < end)) {
+                continue;
+            }
+        }
+
+        // handle special cases for mcu-side operations
+        if (lfs_tag_type3(tag) == LFS_FROM_NOOP) {
+            // do nothing
+        } else if (lfs_tag_type3(tag) == LFS_FROM_MOVE) {
+            uint16_t fromid = lfs_tag_size(tag);
+            uint16_t toid = lfs_tag_id(tag);
+            int err = lfs_dir_traverse(lfs,
+                    buffer, 0, 0xffffffff, NULL, 0,
+                    LFS_MKTAG(0x600, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_STRUCT, 0, 0),
+                    fromid, fromid+1, toid-fromid+diff,
+                    cb, data);
+            if (err) {
+                return err;
+            }
+        } else if (lfs_tag_type3(tag) == LFS_FROM_USERATTRS) {
+            for (unsigned i = 0; i < lfs_tag_size(tag); i++) {
+                const struct lfs_attr *a = buffer;
+                int err = cb(data, LFS_MKTAG(LFS_TYPE_USERATTR + a[i].type,
+                        lfs_tag_id(tag) + diff, a[i].size), a[i].buffer);
+                if (err) {
+                    return err;
+                }
+            }
+        } else {
+            int err = cb(data, tag + LFS_MKTAG(0, diff, 0), buffer);
+            if (err) {
+                return err;
+            }
+        }
+    }
+}
+
+static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs,
+        lfs_mdir_t *dir, const lfs_block_t pair[2],
+        lfs_tag_t fmask, lfs_tag_t ftag, uint16_t *id,
+        int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) {
+    // we can find tag very efficiently during a fetch, since we're already
+    // scanning the entire directory
+    lfs_stag_t besttag = -1;
+
+    // if either block address is invalid we return LFS_ERR_CORRUPT here,
+    // otherwise later writes to the pair could fail
+    if (pair[0] >= lfs->cfg->block_count || pair[1] >= lfs->cfg->block_count) {
+        return LFS_ERR_CORRUPT;
+    }
+
+    // find the block with the most recent revision
+    uint32_t revs[2] = {0, 0};
+    int r = 0;
+    for (int i = 0; i < 2; i++) {
+        int err = lfs_bd_read(lfs,
+                NULL, &lfs->rcache, sizeof(revs[i]),
+                pair[i], 0, &revs[i], sizeof(revs[i]));
+        revs[i] = lfs_fromle32(revs[i]);
+        if (err && err != LFS_ERR_CORRUPT) {
+            return err;
+        }
+
+        if (err != LFS_ERR_CORRUPT &&
+                lfs_scmp(revs[i], revs[(i+1)%2]) > 0) {
+            r = i;
+        }
+    }
+
+    dir->pair[0] = pair[(r+0)%2];
+    dir->pair[1] = pair[(r+1)%2];
+    dir->rev = revs[(r+0)%2];
+    dir->off = 0; // nonzero = found some commits
+
+    // now scan tags to fetch the actual dir and find possible match
+    for (int i = 0; i < 2; i++) {
+        lfs_off_t off = 0;
+        lfs_tag_t ptag = 0xffffffff;
+
+        uint16_t tempcount = 0;
+        lfs_block_t temptail[2] = {LFS_BLOCK_NULL, LFS_BLOCK_NULL};
+        bool tempsplit = false;
+        lfs_stag_t tempbesttag = besttag;
+
+        dir->rev = lfs_tole32(dir->rev);
+        uint32_t crc = lfs_crc(0xffffffff, &dir->rev, sizeof(dir->rev));
+        dir->rev = lfs_fromle32(dir->rev);
+
+        while (true) {
+            // extract next tag
+            lfs_tag_t tag;
+            off += lfs_tag_dsize(ptag);
+            int err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, lfs->cfg->block_size,
+                    dir->pair[0], off, &tag, sizeof(tag));
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    // can't continue?
+                    dir->erased = false;
+                    break;
+                }
+                return err;
+            }
+
+            crc = lfs_crc(crc, &tag, sizeof(tag));
+            tag = lfs_frombe32(tag) ^ ptag;
+
+            // next commit not yet programmed or we're not in valid range
+            if (!lfs_tag_isvalid(tag)) {
+                dir->erased = (lfs_tag_type1(ptag) == LFS_TYPE_CRC &&
+                        dir->off % lfs->cfg->prog_size == 0);
+                break;
+            } else if (off + lfs_tag_dsize(tag) > lfs->cfg->block_size) {
+                dir->erased = false;
+                break;
+            }
+
+            ptag = tag;
+
+            if (lfs_tag_type1(tag) == LFS_TYPE_CRC) {
+                // check the crc attr
+                uint32_t dcrc;
+                err = lfs_bd_read(lfs,
+                        NULL, &lfs->rcache, lfs->cfg->block_size,
+                        dir->pair[0], off+sizeof(tag), &dcrc, sizeof(dcrc));
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        dir->erased = false;
+                        break;
+                    }
+                    return err;
+                }
+                dcrc = lfs_fromle32(dcrc);
+
+                if (crc != dcrc) {
+                    dir->erased = false;
+                    break;
+                }
+
+                // reset the next bit if we need to
+                ptag ^= (lfs_tag_t)(lfs_tag_chunk(tag) & 1U) << 31;
+
+                // toss our crc into the filesystem seed for
+                // pseudorandom numbers
+                lfs->seed ^= crc;
+
+                // update with what's found so far
+                besttag = tempbesttag;
+                dir->off = off + lfs_tag_dsize(tag);
+                dir->etag = ptag;
+                dir->count = tempcount;
+                dir->tail[0] = temptail[0];
+                dir->tail[1] = temptail[1];
+                dir->split = tempsplit;
+
+                // reset crc
+                crc = 0xffffffff;
+                continue;
+            }
+
+            // crc the entry first, hopefully leaving it in the cache
+            for (lfs_off_t j = sizeof(tag); j < lfs_tag_dsize(tag); j++) {
+                uint8_t dat;
+                err = lfs_bd_read(lfs,
+                        NULL, &lfs->rcache, lfs->cfg->block_size,
+                        dir->pair[0], off+j, &dat, 1);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        dir->erased = false;
+                        break;
+                    }
+                    return err;
+                }
+
+                crc = lfs_crc(crc, &dat, 1);
+            }
+
+            // directory modification tags?
+            if (lfs_tag_type1(tag) == LFS_TYPE_NAME) {
+                // increase count of files if necessary
+                if (lfs_tag_id(tag) >= tempcount) {
+                    tempcount = lfs_tag_id(tag) + 1;
+                }
+            } else if (lfs_tag_type1(tag) == LFS_TYPE_SPLICE) {
+                tempcount += lfs_tag_splice(tag);
+
+                if (tag == (LFS_MKTAG(LFS_TYPE_DELETE, 0, 0) |
+                        (LFS_MKTAG(0, 0x3ff, 0) & tempbesttag))) {
+                    tempbesttag |= 0x80000000;
+                } else if (tempbesttag != -1 &&
+                        lfs_tag_id(tag) <= lfs_tag_id(tempbesttag)) {
+                    tempbesttag += LFS_MKTAG(0, lfs_tag_splice(tag), 0);
+                }
+            } else if (lfs_tag_type1(tag) == LFS_TYPE_TAIL) {
+                tempsplit = (lfs_tag_chunk(tag) & 1);
+
+                err = lfs_bd_read(lfs,
+                        NULL, &lfs->rcache, lfs->cfg->block_size,
+                        dir->pair[0], off+sizeof(tag), &temptail, 8);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        dir->erased = false;
+                        break;
+                    }
+                }
+                lfs_pair_fromle32(temptail);
+            }
+
+            // found a match for our fetcher?
+            if ((fmask & tag) == (fmask & ftag)) {
+                int res = cb(data, tag, &(struct lfs_diskoff){
+                        dir->pair[0], off+sizeof(tag)});
+                if (res < 0) {
+                    if (res == LFS_ERR_CORRUPT) {
+                        dir->erased = false;
+                        break;
+                    }
+                    return res;
+                }
+
+                if (res == LFS_CMP_EQ) {
+                    // found a match
+                    tempbesttag = tag;
+                } else if ((LFS_MKTAG(0x7ff, 0x3ff, 0) & tag) ==
+                        (LFS_MKTAG(0x7ff, 0x3ff, 0) & tempbesttag)) {
+                    // found an identical tag, but contents didn't match
+                    // this must mean that our besttag has been overwritten
+                    tempbesttag = -1;
+                } else if (res == LFS_CMP_GT &&
+                        lfs_tag_id(tag) <= lfs_tag_id(tempbesttag)) {
+                    // found a greater match, keep track to keep things sorted
+                    tempbesttag = tag | 0x80000000;
+                }
+            }
+        }
+
+        // consider what we have good enough
+        if (dir->off > 0) {
+            // synthetic move
+            if (lfs_gstate_hasmovehere(&lfs->gdisk, dir->pair)) {
+                if (lfs_tag_id(lfs->gdisk.tag) == lfs_tag_id(besttag)) {
+                    besttag |= 0x80000000;
+                } else if (besttag != -1 &&
+                        lfs_tag_id(lfs->gdisk.tag) < lfs_tag_id(besttag)) {
+                    besttag -= LFS_MKTAG(0, 1, 0);
+                }
+            }
+
+            // found tag? or found best id?
+            if (id) {
+                *id = lfs_min(lfs_tag_id(besttag), dir->count);
+            }
+
+            if (lfs_tag_isvalid(besttag)) {
+                return besttag;
+            } else if (lfs_tag_id(besttag) < dir->count) {
+                return LFS_ERR_NOENT;
+            } else {
+                return 0;
+            }
+        }
+
+        // failed, try the other block?
+        lfs_pair_swap(dir->pair);
+        dir->rev = revs[(r+1)%2];
+    }
+
+    LFS_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}",
+            dir->pair[0], dir->pair[1]);
+    return LFS_ERR_CORRUPT;
+}
+
+static int lfs_dir_fetch(lfs_t *lfs,
+        lfs_mdir_t *dir, const lfs_block_t pair[2]) {
+    // note, mask=-1, tag=-1 can never match a tag since this
+    // pattern has the invalid bit set
+    return (int)lfs_dir_fetchmatch(lfs, dir, pair,
+            (lfs_tag_t)-1, (lfs_tag_t)-1, NULL, NULL, NULL);
+}
+
+static int lfs_dir_getgstate(lfs_t *lfs, const lfs_mdir_t *dir,
+        lfs_gstate_t *gstate) {
+    lfs_gstate_t temp;
+    lfs_stag_t res = lfs_dir_get(lfs, dir, LFS_MKTAG(0x7ff, 0, 0),
+            LFS_MKTAG(LFS_TYPE_MOVESTATE, 0, sizeof(temp)), &temp);
+    if (res < 0 && res != LFS_ERR_NOENT) {
+        return res;
+    }
+
+    if (res != LFS_ERR_NOENT) {
+        // xor together to find resulting gstate
+        lfs_gstate_fromle32(&temp);
+        lfs_gstate_xor(gstate, &temp);
+    }
+
+    return 0;
+}
+
+static int lfs_dir_getinfo(lfs_t *lfs, lfs_mdir_t *dir,
+        uint16_t id, struct lfs_info *info) {
+    if (id == 0x3ff) {
+        // special case for root
+        strcpy(info->name, "/");
+        info->type = LFS_TYPE_DIR;
+        return 0;
+    }
+
+    lfs_stag_t tag = lfs_dir_get(lfs, dir, LFS_MKTAG(0x780, 0x3ff, 0),
+            LFS_MKTAG(LFS_TYPE_NAME, id, lfs->name_max+1), info->name);
+    if (tag < 0) {
+        return (int)tag;
+    }
+
+    info->type = lfs_tag_type3(tag);
+
+    struct lfs_ctz ctz;
+    tag = lfs_dir_get(lfs, dir, LFS_MKTAG(0x700, 0x3ff, 0),
+            LFS_MKTAG(LFS_TYPE_STRUCT, id, sizeof(ctz)), &ctz);
+    if (tag < 0) {
+        return (int)tag;
+    }
+    lfs_ctz_fromle32(&ctz);
+
+    if (lfs_tag_type3(tag) == LFS_TYPE_CTZSTRUCT) {
+        info->size = ctz.size;
+    } else if (lfs_tag_type3(tag) == LFS_TYPE_INLINESTRUCT) {
+        info->size = lfs_tag_size(tag);
+    }
+
+    return 0;
+}
+
+struct lfs_dir_find_match {
+    lfs_t *lfs;
+    const void *name;
+    lfs_size_t size;
+};
+
+static int lfs_dir_find_match(void *data,
+        lfs_tag_t tag, const void *buffer) {
+    struct lfs_dir_find_match *name = data;
+    lfs_t *lfs = name->lfs;
+    const struct lfs_diskoff *disk = buffer;
+
+    // compare with disk
+    lfs_size_t diff = lfs_min(name->size, lfs_tag_size(tag));
+    int res = lfs_bd_cmp(lfs,
+            NULL, &lfs->rcache, diff,
+            disk->block, disk->off, name->name, diff);
+    if (res != LFS_CMP_EQ) {
+        return res;
+    }
+
+    // only equal if our size is still the same
+    if (name->size != lfs_tag_size(tag)) {
+        return (name->size < lfs_tag_size(tag)) ? LFS_CMP_LT : LFS_CMP_GT;
+    }
+
+    // found a match!
+    return LFS_CMP_EQ;
+}
+
+static lfs_stag_t lfs_dir_find(lfs_t *lfs, lfs_mdir_t *dir,
+        const char **path, uint16_t *id) {
+    // we reduce path to a single name if we can find it
+    const char *name = *path;
+    if (id) {
+        *id = 0x3ff;
+    }
+
+    // default to root dir
+    lfs_stag_t tag = LFS_MKTAG(LFS_TYPE_DIR, 0x3ff, 0);
+    dir->tail[0] = lfs->root[0];
+    dir->tail[1] = lfs->root[1];
+
+    while (true) {
+nextname:
+        // skip slashes
+        name += strspn(name, "/");
+        lfs_size_t namelen = strcspn(name, "/");
+
+        // skip '.' and root '..'
+        if ((namelen == 1 && memcmp(name, ".", 1) == 0) ||
+            (namelen == 2 && memcmp(name, "..", 2) == 0)) {
+            name += namelen;
+            goto nextname;
+        }
+
+        // skip if matched by '..' in name
+        const char *suffix = name + namelen;
+        lfs_size_t sufflen;
+        int depth = 1;
+        while (true) {
+            suffix += strspn(suffix, "/");
+            sufflen = strcspn(suffix, "/");
+            if (sufflen == 0) {
+                break;
+            }
+
+            if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) {
+                depth -= 1;
+                if (depth == 0) {
+                    name = suffix + sufflen;
+                    goto nextname;
+                }
+            } else {
+                depth += 1;
+            }
+
+            suffix += sufflen;
+        }
+
+        // found path
+        if (name[0] == '\0') {
+            return tag;
+        }
+
+        // update what we've found so far
+        *path = name;
+
+        // only continue if we hit a directory
+        if (lfs_tag_type3(tag) != LFS_TYPE_DIR) {
+            return LFS_ERR_NOTDIR;
+        }
+
+        // grab the entry data
+        if (lfs_tag_id(tag) != 0x3ff) {
+            lfs_stag_t res = lfs_dir_get(lfs, dir, LFS_MKTAG(0x700, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), dir->tail);
+            if (res < 0) {
+                return res;
+            }
+            lfs_pair_fromle32(dir->tail);
+        }
+
+        // find entry matching name
+        while (true) {
+            tag = lfs_dir_fetchmatch(lfs, dir, dir->tail,
+                    LFS_MKTAG(0x780, 0, 0),
+                    LFS_MKTAG(LFS_TYPE_NAME, 0, namelen),
+                     // are we last name?
+                    (strchr(name, '/') == NULL) ? id : NULL,
+                    lfs_dir_find_match, &(struct lfs_dir_find_match){
+                        lfs, name, namelen});
+            if (tag < 0) {
+                return tag;
+            }
+
+            if (tag) {
+                break;
+            }
+
+            if (!dir->split) {
+                return LFS_ERR_NOENT;
+            }
+        }
+
+        // to next name
+        name += namelen;
+    }
+}
+
+// commit logic
+struct lfs_commit {
+    lfs_block_t block;
+    lfs_off_t off;
+    lfs_tag_t ptag;
+    uint32_t crc;
+
+    lfs_off_t begin;
+    lfs_off_t end;
+};
+
+static int lfs_dir_commitprog(lfs_t *lfs, struct lfs_commit *commit,
+        const void *buffer, lfs_size_t size) {
+    int err = lfs_bd_prog(lfs,
+            &lfs->pcache, &lfs->rcache, false,
+            commit->block, commit->off ,
+            (const uint8_t*)buffer, size);
+    if (err) {
+        return err;
+    }
+
+    commit->crc = lfs_crc(commit->crc, buffer, size);
+    commit->off += size;
+    return 0;
+}
+
+static int lfs_dir_commitattr(lfs_t *lfs, struct lfs_commit *commit,
+        lfs_tag_t tag, const void *buffer) {
+    // check if we fit
+    lfs_size_t dsize = lfs_tag_dsize(tag);
+    if (commit->off + dsize > commit->end) {
+        return LFS_ERR_NOSPC;
+    }
+
+    // write out tag
+    lfs_tag_t ntag = lfs_tobe32((tag & 0x7fffffff) ^ commit->ptag);
+    int err = lfs_dir_commitprog(lfs, commit, &ntag, sizeof(ntag));
+    if (err) {
+        return err;
+    }
+
+    if (!(tag & 0x80000000)) {
+        // from memory
+        err = lfs_dir_commitprog(lfs, commit, buffer, dsize-sizeof(tag));
+        if (err) {
+            return err;
+        }
+    } else {
+        // from disk
+        const struct lfs_diskoff *disk = buffer;
+        for (lfs_off_t i = 0; i < dsize-sizeof(tag); i++) {
+            // rely on caching to make this efficient
+            uint8_t dat;
+            err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, dsize-sizeof(tag)-i,
+                    disk->block, disk->off+i, &dat, 1);
+            if (err) {
+                return err;
+            }
+
+            err = lfs_dir_commitprog(lfs, commit, &dat, 1);
+            if (err) {
+                return err;
+            }
+        }
+    }
+
+    commit->ptag = tag & 0x7fffffff;
+    return 0;
+}
+
+static int lfs_dir_commitcrc(lfs_t *lfs, struct lfs_commit *commit) {
+    const lfs_off_t off1 = commit->off;
+    const uint32_t crc1 = commit->crc;
+    // align to program units
+    const lfs_off_t end = lfs_alignup(off1 + 2*sizeof(uint32_t),
+            lfs->cfg->prog_size);
+
+    // create crc tags to fill up remainder of commit, note that
+    // padding is not crced, which lets fetches skip padding but
+    // makes committing a bit more complicated
+    while (commit->off < end) {
+        lfs_off_t off = commit->off + sizeof(lfs_tag_t);
+        lfs_off_t noff = lfs_min(end - off, 0x3fe) + off;
+        if (noff < end) {
+            noff = lfs_min(noff, end - 2*sizeof(uint32_t));
+        }
+
+        // read erased state from next program unit
+        lfs_tag_t tag = 0xffffffff;
+        int err = lfs_bd_read(lfs,
+                NULL, &lfs->rcache, sizeof(tag),
+                commit->block, noff, &tag, sizeof(tag));
+        if (err && err != LFS_ERR_CORRUPT) {
+            return err;
+        }
+
+        // build crc tag
+        bool reset = ~lfs_frombe32(tag) >> 31;
+        tag = LFS_MKTAG(LFS_TYPE_CRC + reset, 0x3ff, noff - off);
+
+        // write out crc
+        uint32_t footer[2];
+        footer[0] = lfs_tobe32(tag ^ commit->ptag);
+        commit->crc = lfs_crc(commit->crc, &footer[0], sizeof(footer[0]));
+        footer[1] = lfs_tole32(commit->crc);
+        err = lfs_bd_prog(lfs,
+                &lfs->pcache, &lfs->rcache, false,
+                commit->block, commit->off, &footer, sizeof(footer));
+        if (err) {
+            return err;
+        }
+
+        commit->off += sizeof(tag)+lfs_tag_size(tag);
+        commit->ptag = tag ^ ((lfs_tag_t)reset << 31);
+        commit->crc = 0xffffffff; // reset crc for next "commit"
+    }
+
+    // flush buffers
+    int err = lfs_bd_sync(lfs, &lfs->pcache, &lfs->rcache, false);
+    if (err) {
+        return err;
+    }
+
+    // successful commit, check checksums to make sure
+    lfs_off_t off = commit->begin;
+    lfs_off_t noff = off1 + sizeof(uint32_t);
+    while (off < end) {
+        uint32_t crc = 0xffffffff;
+        for (lfs_off_t i = off; i < noff+sizeof(uint32_t); i++) {
+            // check against written crc, may catch blocks that
+            // become readonly and match our commit size exactly
+            if (i == off1 && crc != crc1) {
+                return LFS_ERR_CORRUPT;
+            }
+
+            // leave it up to caching to make this efficient
+            uint8_t dat;
+            err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, noff+sizeof(uint32_t)-i,
+                    commit->block, i, &dat, 1);
+            if (err) {
+                return err;
+            }
+
+            crc = lfs_crc(crc, &dat, 1);
+        }
+
+        // detected write error?
+        if (crc != 0) {
+            return LFS_ERR_CORRUPT;
+        }
+
+        // skip padding
+        off = lfs_min(end - noff, 0x3fe) + noff;
+        if (off < end) {
+            off = lfs_min(off, end - 2*sizeof(uint32_t));
+        }
+        noff = off + sizeof(uint32_t);
+    }
+
+    return 0;
+}
+
+static int lfs_dir_alloc(lfs_t *lfs, lfs_mdir_t *dir) {
+    // allocate pair of dir blocks (backwards, so we write block 1 first)
+    for (int i = 0; i < 2; i++) {
+        int err = lfs_alloc(lfs, &dir->pair[(i+1)%2]);
+        if (err) {
+            return err;
+        }
+    }
+
+    // zero for reproducability in case initial block is unreadable
+    dir->rev = 0;
+
+    // rather than clobbering one of the blocks we just pretend
+    // the revision may be valid
+    int err = lfs_bd_read(lfs,
+            NULL, &lfs->rcache, sizeof(dir->rev),
+            dir->pair[0], 0, &dir->rev, sizeof(dir->rev));
+    dir->rev = lfs_fromle32(dir->rev);
+    if (err && err != LFS_ERR_CORRUPT) {
+        return err;
+    }
+
+    // make sure we don't immediately evict
+    dir->rev += dir->rev & 1;
+
+    // set defaults
+    dir->off = sizeof(dir->rev);
+    dir->etag = 0xffffffff;
+    dir->count = 0;
+    dir->tail[0] = LFS_BLOCK_NULL;
+    dir->tail[1] = LFS_BLOCK_NULL;
+    dir->erased = false;
+    dir->split = false;
+
+    // don't write out yet, let caller take care of that
+    return 0;
+}
+
+static int lfs_dir_drop(lfs_t *lfs, lfs_mdir_t *dir, lfs_mdir_t *tail) {
+    // steal state
+    int err = lfs_dir_getgstate(lfs, tail, &lfs->gdelta);
+    if (err) {
+        return err;
+    }
+
+    // steal tail
+    lfs_pair_tole32(tail->tail);
+    err = lfs_dir_commit(lfs, dir, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_TAIL + tail->split, 0x3ff, 8), tail->tail}));
+    lfs_pair_fromle32(tail->tail);
+    if (err) {
+        return err;
+    }
+
+    return 0;
+}
+
+static int lfs_dir_split(lfs_t *lfs,
+        lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount,
+        lfs_mdir_t *source, uint16_t split, uint16_t end) {
+    // create tail directory
+    lfs_alloc_ack(lfs);
+    lfs_mdir_t tail;
+    int err = lfs_dir_alloc(lfs, &tail);
+    if (err) {
+        return err;
+    }
+
+    tail.split = dir->split;
+    tail.tail[0] = dir->tail[0];
+    tail.tail[1] = dir->tail[1];
+
+    err = lfs_dir_compact(lfs, &tail, attrs, attrcount, source, split, end);
+    if (err) {
+        return err;
+    }
+
+    dir->tail[0] = tail.pair[0];
+    dir->tail[1] = tail.pair[1];
+    dir->split = true;
+
+    // update root if needed
+    if (lfs_pair_cmp(dir->pair, lfs->root) == 0 && split == 0) {
+        lfs->root[0] = tail.pair[0];
+        lfs->root[1] = tail.pair[1];
+    }
+
+    return 0;
+}
+
+static int lfs_dir_commit_size(void *p, lfs_tag_t tag, const void *buffer) {
+    lfs_size_t *size = p;
+    (void)buffer;
+
+    *size += lfs_tag_dsize(tag);
+    return 0;
+}
+
+struct lfs_dir_commit_commit {
+    lfs_t *lfs;
+    struct lfs_commit *commit;
+};
+
+static int lfs_dir_commit_commit(void *p, lfs_tag_t tag, const void *buffer) {
+    struct lfs_dir_commit_commit *commit = p;
+    return lfs_dir_commitattr(commit->lfs, commit->commit, tag, buffer);
+}
+
+static int lfs_dir_compact(lfs_t *lfs,
+        lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount,
+        lfs_mdir_t *source, uint16_t begin, uint16_t end) {
+    // save some state in case block is bad
+    const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]};
+    bool relocated = false;
+    bool tired = false;
+
+    // should we split?
+    while (end - begin > 1) {
+        // find size
+        lfs_size_t size = 0;
+        int err = lfs_dir_traverse(lfs,
+                source, 0, 0xffffffff, attrs, attrcount,
+                LFS_MKTAG(0x400, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_NAME, 0, 0),
+                begin, end, -begin,
+                lfs_dir_commit_size, &size);
+        if (err) {
+            return err;
+        }
+
+        // space is complicated, we need room for tail, crc, gstate,
+        // cleanup delete, and we cap at half a block to give room
+        // for metadata updates.
+        if (end - begin < 0xff &&
+                size <= lfs_min(lfs->cfg->block_size - 36,
+                    lfs_alignup(lfs->cfg->block_size/2,
+                        lfs->cfg->prog_size))) {
+            break;
+        }
+
+        // can't fit, need to split, we should really be finding the
+        // largest size that fits with a small binary search, but right now
+        // it's not worth the code size
+        uint16_t split = (end - begin) / 2;
+        err = lfs_dir_split(lfs, dir, attrs, attrcount,
+                source, begin+split, end);
+        if (err) {
+            // if we fail to split, we may be able to overcompact, unless
+            // we're too big for even the full block, in which case our
+            // only option is to error
+            if (err == LFS_ERR_NOSPC && size <= lfs->cfg->block_size - 36) {
+                break;
+            }
+            return err;
+        }
+
+        end = begin + split;
+    }
+
+    // increment revision count
+    dir->rev += 1;
+    // If our revision count == n * block_cycles, we should force a relocation,
+    // this is how littlefs wear-levels at the metadata-pair level. Note that we
+    // actually use (block_cycles+1)|1, this is to avoid two corner cases:
+    // 1. block_cycles = 1, which would prevent relocations from terminating
+    // 2. block_cycles = 2n, which, due to aliasing, would only ever relocate
+    //    one metadata block in the pair, effectively making this useless
+    if (lfs->cfg->block_cycles > 0 &&
+            (dir->rev % ((lfs->cfg->block_cycles+1)|1) == 0)) {
+        if (lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) == 0) {
+            // oh no! we're writing too much to the superblock,
+            // should we expand?
+            lfs_ssize_t res = lfs_fs_size(lfs);
+            if (res < 0) {
+                return res;
+            }
+
+            // do we have extra space? littlefs can't reclaim this space
+            // by itself, so expand cautiously
+            if ((lfs_size_t)res < lfs->cfg->block_count/2) {
+                LFS_DEBUG("Expanding superblock at rev %"PRIu32, dir->rev);
+                int err = lfs_dir_split(lfs, dir, attrs, attrcount,
+                        source, begin, end);
+                if (err && err != LFS_ERR_NOSPC) {
+                    return err;
+                }
+
+                // welp, we tried, if we ran out of space there's not much
+                // we can do, we'll error later if we've become frozen
+                if (!err) {
+                    end = begin;
+                }
+            }
+#ifdef LFS_MIGRATE
+        } else if (lfs->lfs1) {
+            // do not proactively relocate blocks during migrations, this
+            // can cause a number of failure states such: clobbering the
+            // v1 superblock if we relocate root, and invalidating directory
+            // pointers if we relocate the head of a directory. On top of
+            // this, relocations increase the overall complexity of
+            // lfs_migration, which is already a delicate operation.
+#endif
+        } else {
+            // we're writing too much, time to relocate
+            tired = true;
+            goto relocate;
+        }
+    }
+
+    // begin loop to commit compaction to blocks until a compact sticks
+    while (true) {
+        {
+            // setup commit state
+            struct lfs_commit commit = {
+                .block = dir->pair[1],
+                .off = 0,
+                .ptag = 0xffffffff,
+                .crc = 0xffffffff,
+
+                .begin = 0,
+                .end = lfs->cfg->block_size - 8,
+            };
+
+            // erase block to write to
+            int err = lfs_bd_erase(lfs, dir->pair[1]);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            // write out header
+            dir->rev = lfs_tole32(dir->rev);
+            err = lfs_dir_commitprog(lfs, &commit,
+                    &dir->rev, sizeof(dir->rev));
+            dir->rev = lfs_fromle32(dir->rev);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            // traverse the directory, this time writing out all unique tags
+            err = lfs_dir_traverse(lfs,
+                    source, 0, 0xffffffff, attrs, attrcount,
+                    LFS_MKTAG(0x400, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_NAME, 0, 0),
+                    begin, end, -begin,
+                    lfs_dir_commit_commit, &(struct lfs_dir_commit_commit){
+                        lfs, &commit});
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            // commit tail, which may be new after last size check
+            if (!lfs_pair_isnull(dir->tail)) {
+                lfs_pair_tole32(dir->tail);
+                err = lfs_dir_commitattr(lfs, &commit,
+                        LFS_MKTAG(LFS_TYPE_TAIL + dir->split, 0x3ff, 8),
+                        dir->tail);
+                lfs_pair_fromle32(dir->tail);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        goto relocate;
+                    }
+                    return err;
+                }
+            }
+
+            // bring over gstate?
+            lfs_gstate_t delta = {0};
+            if (!relocated) {
+                lfs_gstate_xor(&delta, &lfs->gdisk);
+                lfs_gstate_xor(&delta, &lfs->gstate);
+            }
+            lfs_gstate_xor(&delta, &lfs->gdelta);
+            delta.tag &= ~LFS_MKTAG(0, 0, 0x3ff);
+
+            err = lfs_dir_getgstate(lfs, dir, &delta);
+            if (err) {
+                return err;
+            }
+
+            if (!lfs_gstate_iszero(&delta)) {
+                lfs_gstate_tole32(&delta);
+                err = lfs_dir_commitattr(lfs, &commit,
+                        LFS_MKTAG(LFS_TYPE_MOVESTATE, 0x3ff,
+                            sizeof(delta)), &delta);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        goto relocate;
+                    }
+                    return err;
+                }
+            }
+
+            // complete commit with crc
+            err = lfs_dir_commitcrc(lfs, &commit);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            // successful compaction, swap dir pair to indicate most recent
+            LFS_ASSERT(commit.off % lfs->cfg->prog_size == 0);
+            lfs_pair_swap(dir->pair);
+            dir->count = end - begin;
+            dir->off = commit.off;
+            dir->etag = commit.ptag;
+            // update gstate
+            lfs->gdelta = (lfs_gstate_t){0};
+            if (!relocated) {
+                lfs->gdisk = lfs->gstate;
+            }
+        }
+        break;
+
+relocate:
+        // commit was corrupted, drop caches and prepare to relocate block
+        relocated = true;
+        lfs_cache_drop(lfs, &lfs->pcache);
+        if (!tired) {
+            LFS_DEBUG("Bad block at 0x%"PRIx32, dir->pair[1]);
+        }
+
+        // can't relocate superblock, filesystem is now frozen
+        if (lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) == 0) {
+            LFS_WARN("Superblock 0x%"PRIx32" has become unwritable",
+                    dir->pair[1]);
+            return LFS_ERR_NOSPC;
+        }
+
+        // relocate half of pair
+        int err = lfs_alloc(lfs, &dir->pair[1]);
+        if (err && (err != LFS_ERR_NOSPC || !tired)) {
+            return err;
+        }
+
+        tired = false;
+        continue;
+    }
+
+    if (relocated) {
+        // update references if we relocated
+        LFS_DEBUG("Relocating {0x%"PRIx32", 0x%"PRIx32"} "
+                    "-> {0x%"PRIx32", 0x%"PRIx32"}",
+                oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]);
+        int err = lfs_fs_relocate(lfs, oldpair, dir->pair);
+        if (err) {
+            return err;
+        }
+    }
+
+    return 0;
+}
+
+static int lfs_dir_commit(lfs_t *lfs, lfs_mdir_t *dir,
+        const struct lfs_mattr *attrs, int attrcount) {
+    // check for any inline files that aren't RAM backed and
+    // forcefully evict them, needed for filesystem consistency
+    for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) {
+        if (dir != &f->m && lfs_pair_cmp(f->m.pair, dir->pair) == 0 &&
+                f->type == LFS_TYPE_REG && (f->flags & LFS_F_INLINE) &&
+                f->ctz.size > lfs->cfg->cache_size) {
+            int err = lfs_file_outline(lfs, f);
+            if (err) {
+                return err;
+            }
+
+            err = lfs_file_flush(lfs, f);
+            if (err) {
+                return err;
+            }
+        }
+    }
+
+    // calculate changes to the directory
+    lfs_mdir_t olddir = *dir;
+    bool hasdelete = false;
+    for (int i = 0; i < attrcount; i++) {
+        if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_CREATE) {
+            dir->count += 1;
+        } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE) {
+            LFS_ASSERT(dir->count > 0);
+            dir->count -= 1;
+            hasdelete = true;
+        } else if (lfs_tag_type1(attrs[i].tag) == LFS_TYPE_TAIL) {
+            dir->tail[0] = ((lfs_block_t*)attrs[i].buffer)[0];
+            dir->tail[1] = ((lfs_block_t*)attrs[i].buffer)[1];
+            dir->split = (lfs_tag_chunk(attrs[i].tag) & 1);
+            lfs_pair_fromle32(dir->tail);
+        }
+    }
+
+    // should we actually drop the directory block?
+    if (hasdelete && dir->count == 0) {
+        lfs_mdir_t pdir;
+        int err = lfs_fs_pred(lfs, dir->pair, &pdir);
+        if (err && err != LFS_ERR_NOENT) {
+            *dir = olddir;
+            return err;
+        }
+
+        if (err != LFS_ERR_NOENT && pdir.split) {
+            err = lfs_dir_drop(lfs, &pdir, dir);
+            if (err) {
+                *dir = olddir;
+                return err;
+            }
+        }
+    }
+
+    if (dir->erased || dir->count >= 0xff) {
+        // try to commit
+        struct lfs_commit commit = {
+            .block = dir->pair[0],
+            .off = dir->off,
+            .ptag = dir->etag,
+            .crc = 0xffffffff,
+
+            .begin = dir->off,
+            .end = lfs->cfg->block_size - 8,
+        };
+
+        // traverse attrs that need to be written out
+        lfs_pair_tole32(dir->tail);
+        int err = lfs_dir_traverse(lfs,
+                dir, dir->off, dir->etag, attrs, attrcount,
+                0, 0, 0, 0, 0,
+                lfs_dir_commit_commit, &(struct lfs_dir_commit_commit){
+                    lfs, &commit});
+        lfs_pair_fromle32(dir->tail);
+        if (err) {
+            if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) {
+                goto compact;
+            }
+            *dir = olddir;
+            return err;
+        }
+
+        // commit any global diffs if we have any
+        lfs_gstate_t delta = {0};
+        lfs_gstate_xor(&delta, &lfs->gstate);
+        lfs_gstate_xor(&delta, &lfs->gdisk);
+        lfs_gstate_xor(&delta, &lfs->gdelta);
+        delta.tag &= ~LFS_MKTAG(0, 0, 0x3ff);
+        if (!lfs_gstate_iszero(&delta)) {
+            err = lfs_dir_getgstate(lfs, dir, &delta);
+            if (err) {
+                *dir = olddir;
+                return err;
+            }
+
+            lfs_gstate_tole32(&delta);
+            err = lfs_dir_commitattr(lfs, &commit,
+                    LFS_MKTAG(LFS_TYPE_MOVESTATE, 0x3ff,
+                        sizeof(delta)), &delta);
+            if (err) {
+                if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) {
+                    goto compact;
+                }
+                *dir = olddir;
+                return err;
+            }
+        }
+
+        // finalize commit with the crc
+        err = lfs_dir_commitcrc(lfs, &commit);
+        if (err) {
+            if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) {
+                goto compact;
+            }
+            *dir = olddir;
+            return err;
+        }
+
+        // successful commit, update dir
+        LFS_ASSERT(commit.off % lfs->cfg->prog_size == 0);
+        dir->off = commit.off;
+        dir->etag = commit.ptag;
+        // and update gstate
+        lfs->gdisk = lfs->gstate;
+        lfs->gdelta = (lfs_gstate_t){0};
+    } else {
+compact:
+        // fall back to compaction
+        lfs_cache_drop(lfs, &lfs->pcache);
+
+        int err = lfs_dir_compact(lfs, dir, attrs, attrcount,
+                dir, 0, dir->count);
+        if (err) {
+            *dir = olddir;
+            return err;
+        }
+    }
+
+    // this complicated bit of logic is for fixing up any active
+    // metadata-pairs that we may have affected
+    //
+    // note we have to make two passes since the mdir passed to
+    // lfs_dir_commit could also be in this list, and even then
+    // we need to copy the pair so they don't get clobbered if we refetch
+    // our mdir.
+    for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) {
+        if (&d->m != dir && lfs_pair_cmp(d->m.pair, olddir.pair) == 0) {
+            d->m = *dir;
+            for (int i = 0; i < attrcount; i++) {
+                if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE &&
+                        d->id == lfs_tag_id(attrs[i].tag)) {
+                    d->m.pair[0] = LFS_BLOCK_NULL;
+                    d->m.pair[1] = LFS_BLOCK_NULL;
+                } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE &&
+                        d->id > lfs_tag_id(attrs[i].tag)) {
+                    d->id -= 1;
+                    if (d->type == LFS_TYPE_DIR) {
+                        ((lfs_dir_t*)d)->pos -= 1;
+                    }
+                } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_CREATE &&
+                        d->id >= lfs_tag_id(attrs[i].tag)) {
+                    d->id += 1;
+                    if (d->type == LFS_TYPE_DIR) {
+                        ((lfs_dir_t*)d)->pos += 1;
+                    }
+                }
+            }
+        }
+    }
+
+    for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) {
+        if (lfs_pair_cmp(d->m.pair, olddir.pair) == 0) {
+            while (d->id >= d->m.count && d->m.split) {
+                // we split and id is on tail now
+                d->id -= d->m.count;
+                int err = lfs_dir_fetch(lfs, &d->m, d->m.tail);
+                if (err) {
+                    return err;
+                }
+            }
+        }
+    }
+
+    return 0;
+}
+
+
+/// Top level directory operations ///
+int lfs_mkdir(lfs_t *lfs, const char *path) {
+    LFS_TRACE("lfs_mkdir(%p, \"%s\")", (void*)lfs, path);
+    // deorphan if we haven't yet, needed at most once after poweron
+    int err = lfs_fs_forceconsistency(lfs);
+    if (err) {
+        LFS_TRACE("lfs_mkdir -> %d", err);
+        return err;
+    }
+
+    struct lfs_mlist cwd;
+    cwd.next = lfs->mlist;
+    uint16_t id;
+    err = lfs_dir_find(lfs, &cwd.m, &path, &id);
+    if (!(err == LFS_ERR_NOENT && id != 0x3ff)) {
+        LFS_TRACE("lfs_mkdir -> %d", (err < 0) ? err : LFS_ERR_EXIST);
+        return (err < 0) ? err : LFS_ERR_EXIST;
+    }
+
+    // check that name fits
+    lfs_size_t nlen = strlen(path);
+    if (nlen > lfs->name_max) {
+        LFS_TRACE("lfs_mkdir -> %d", LFS_ERR_NAMETOOLONG);
+        return LFS_ERR_NAMETOOLONG;
+    }
+
+    // build up new directory
+    lfs_alloc_ack(lfs);
+    lfs_mdir_t dir;
+    err = lfs_dir_alloc(lfs, &dir);
+    if (err) {
+        LFS_TRACE("lfs_mkdir -> %d", err);
+        return err;
+    }
+
+    // find end of list
+    lfs_mdir_t pred = cwd.m;
+    while (pred.split) {
+        err = lfs_dir_fetch(lfs, &pred, pred.tail);
+        if (err) {
+            LFS_TRACE("lfs_mkdir -> %d", err);
+            return err;
+        }
+    }
+
+    // setup dir
+    lfs_pair_tole32(pred.tail);
+    err = lfs_dir_commit(lfs, &dir, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), pred.tail}));
+    lfs_pair_fromle32(pred.tail);
+    if (err) {
+        LFS_TRACE("lfs_mkdir -> %d", err);
+        return err;
+    }
+
+    // current block end of list?
+    if (cwd.m.split) {
+        // update tails, this creates a desync
+        lfs_fs_preporphans(lfs, +1);
+
+        // it's possible our predecessor has to be relocated, and if
+        // our parent is our predecessor's predecessor, this could have
+        // caused our parent to go out of date, fortunately we can hook
+        // ourselves into littlefs to catch this
+        cwd.type = 0;
+        cwd.id = 0;
+        lfs->mlist = &cwd;
+
+        lfs_pair_tole32(dir.pair);
+        err = lfs_dir_commit(lfs, &pred, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair}));
+        lfs_pair_fromle32(dir.pair);
+        if (err) {
+            lfs->mlist = cwd.next;
+            LFS_TRACE("lfs_mkdir -> %d", err);
+            return err;
+        }
+
+        lfs->mlist = cwd.next;
+        lfs_fs_preporphans(lfs, -1);
+    }
+
+    // now insert into our parent block
+    lfs_pair_tole32(dir.pair);
+    err = lfs_dir_commit(lfs, &cwd.m, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_CREATE, id, 0), NULL},
+            {LFS_MKTAG(LFS_TYPE_DIR, id, nlen), path},
+            {LFS_MKTAG(LFS_TYPE_DIRSTRUCT, id, 8), dir.pair},
+            {LFS_MKTAG_IF(!cwd.m.split,
+                LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair}));
+    lfs_pair_fromle32(dir.pair);
+    if (err) {
+        LFS_TRACE("lfs_mkdir -> %d", err);
+        return err;
+    }
+
+    LFS_TRACE("lfs_mkdir -> %d", 0);
+    return 0;
+}
+
+int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) {
+    LFS_TRACE("lfs_dir_open(%p, %p, \"%s\")", (void*)lfs, (void*)dir, path);
+    lfs_stag_t tag = lfs_dir_find(lfs, &dir->m, &path, NULL);
+    if (tag < 0) {
+        LFS_TRACE("lfs_dir_open -> %"PRId32, tag);
+        return tag;
+    }
+
+    if (lfs_tag_type3(tag) != LFS_TYPE_DIR) {
+        LFS_TRACE("lfs_dir_open -> %d", LFS_ERR_NOTDIR);
+        return LFS_ERR_NOTDIR;
+    }
+
+    lfs_block_t pair[2];
+    if (lfs_tag_id(tag) == 0x3ff) {
+        // handle root dir separately
+        pair[0] = lfs->root[0];
+        pair[1] = lfs->root[1];
+    } else {
+        // get dir pair from parent
+        lfs_stag_t res = lfs_dir_get(lfs, &dir->m, LFS_MKTAG(0x700, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair);
+        if (res < 0) {
+            LFS_TRACE("lfs_dir_open -> %"PRId32, res);
+            return res;
+        }
+        lfs_pair_fromle32(pair);
+    }
+
+    // fetch first pair
+    int err = lfs_dir_fetch(lfs, &dir->m, pair);
+    if (err) {
+        LFS_TRACE("lfs_dir_open -> %d", err);
+        return err;
+    }
+
+    // setup entry
+    dir->head[0] = dir->m.pair[0];
+    dir->head[1] = dir->m.pair[1];
+    dir->id = 0;
+    dir->pos = 0;
+
+    // add to list of mdirs
+    dir->type = LFS_TYPE_DIR;
+    dir->next = (lfs_dir_t*)lfs->mlist;
+    lfs->mlist = (struct lfs_mlist*)dir;
+
+    LFS_TRACE("lfs_dir_open -> %d", 0);
+    return 0;
+}
+
+int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) {
+    LFS_TRACE("lfs_dir_close(%p, %p)", (void*)lfs, (void*)dir);
+    // remove from list of mdirs
+    for (struct lfs_mlist **p = &lfs->mlist; *p; p = &(*p)->next) {
+        if (*p == (struct lfs_mlist*)dir) {
+            *p = (*p)->next;
+            break;
+        }
+    }
+
+    LFS_TRACE("lfs_dir_close -> %d", 0);
+    return 0;
+}
+
+int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) {
+    LFS_TRACE("lfs_dir_read(%p, %p, %p)",
+            (void*)lfs, (void*)dir, (void*)info);
+    memset(info, 0, sizeof(*info));
+
+    // special offset for '.' and '..'
+    if (dir->pos == 0) {
+        info->type = LFS_TYPE_DIR;
+        strcpy(info->name, ".");
+        dir->pos += 1;
+        LFS_TRACE("lfs_dir_read -> %d", true);
+        return true;
+    } else if (dir->pos == 1) {
+        info->type = LFS_TYPE_DIR;
+        strcpy(info->name, "..");
+        dir->pos += 1;
+        LFS_TRACE("lfs_dir_read -> %d", true);
+        return true;
+    }
+
+    while (true) {
+        if (dir->id == dir->m.count) {
+            if (!dir->m.split) {
+                LFS_TRACE("lfs_dir_read -> %d", false);
+                return false;
+            }
+
+            int err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail);
+            if (err) {
+                LFS_TRACE("lfs_dir_read -> %d", err);
+                return err;
+            }
+
+            dir->id = 0;
+        }
+
+        int err = lfs_dir_getinfo(lfs, &dir->m, dir->id, info);
+        if (err && err != LFS_ERR_NOENT) {
+            LFS_TRACE("lfs_dir_read -> %d", err);
+            return err;
+        }
+
+        dir->id += 1;
+        if (err != LFS_ERR_NOENT) {
+            break;
+        }
+    }
+
+    dir->pos += 1;
+    LFS_TRACE("lfs_dir_read -> %d", true);
+    return true;
+}
+
+int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) {
+    LFS_TRACE("lfs_dir_seek(%p, %p, %"PRIu32")",
+            (void*)lfs, (void*)dir, off);
+    // simply walk from head dir
+    int err = lfs_dir_rewind(lfs, dir);
+    if (err) {
+        LFS_TRACE("lfs_dir_seek -> %d", err);
+        return err;
+    }
+
+    // first two for ./..
+    dir->pos = lfs_min(2, off);
+    off -= dir->pos;
+
+    // skip superblock entry
+    dir->id = (off > 0 && lfs_pair_cmp(dir->head, lfs->root) == 0);
+
+    while (off > 0) {
+        int diff = lfs_min(dir->m.count - dir->id, off);
+        dir->id += diff;
+        dir->pos += diff;
+        off -= diff;
+
+        if (dir->id == dir->m.count) {
+            if (!dir->m.split) {
+                LFS_TRACE("lfs_dir_seek -> %d", LFS_ERR_INVAL);
+                return LFS_ERR_INVAL;
+            }
+
+            err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail);
+            if (err) {
+                LFS_TRACE("lfs_dir_seek -> %d", err);
+                return err;
+            }
+
+            dir->id = 0;
+        }
+    }
+
+    LFS_TRACE("lfs_dir_seek -> %d", 0);
+    return 0;
+}
+
+lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir) {
+    LFS_TRACE("lfs_dir_tell(%p, %p)", (void*)lfs, (void*)dir);
+    (void)lfs;
+    LFS_TRACE("lfs_dir_tell -> %"PRId32, dir->pos);
+    return dir->pos;
+}
+
+int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) {
+    LFS_TRACE("lfs_dir_rewind(%p, %p)", (void*)lfs, (void*)dir);
+    // reload the head dir
+    int err = lfs_dir_fetch(lfs, &dir->m, dir->head);
+    if (err) {
+        LFS_TRACE("lfs_dir_rewind -> %d", err);
+        return err;
+    }
+
+    dir->id = 0;
+    dir->pos = 0;
+    LFS_TRACE("lfs_dir_rewind -> %d", 0);
+    return 0;
+}
+
+
+/// File index list operations ///
+static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) {
+    lfs_off_t size = *off;
+    lfs_off_t b = lfs->cfg->block_size - 2*4;
+    lfs_off_t i = size / b;
+    if (i == 0) {
+        return 0;
+    }
+
+    i = (size - 4*(lfs_popc(i-1)+2)) / b;
+    *off = size - b*i - 4*lfs_popc(i);
+    return i;
+}
+
+static int lfs_ctz_find(lfs_t *lfs,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache,
+        lfs_block_t head, lfs_size_t size,
+        lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) {
+    if (size == 0) {
+        *block = LFS_BLOCK_NULL;
+        *off = 0;
+        return 0;
+    }
+
+    lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size-1});
+    lfs_off_t target = lfs_ctz_index(lfs, &pos);
+
+    while (current > target) {
+        lfs_size_t skip = lfs_min(
+                lfs_npw2(current-target+1) - 1,
+                lfs_ctz(current));
+
+        int err = lfs_bd_read(lfs,
+                pcache, rcache, sizeof(head),
+                head, 4*skip, &head, sizeof(head));
+        head = lfs_fromle32(head);
+        if (err) {
+            return err;
+        }
+
+        current -= 1 << skip;
+    }
+
+    *block = head;
+    *off = pos;
+    return 0;
+}
+
+static int lfs_ctz_extend(lfs_t *lfs,
+        lfs_cache_t *pcache, lfs_cache_t *rcache,
+        lfs_block_t head, lfs_size_t size,
+        lfs_block_t *block, lfs_off_t *off) {
+    while (true) {
+        // go ahead and grab a block
+        lfs_block_t nblock;
+        int err = lfs_alloc(lfs, &nblock);
+        if (err) {
+            return err;
+        }
+
+        {
+            err = lfs_bd_erase(lfs, nblock);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            if (size == 0) {
+                *block = nblock;
+                *off = 0;
+                return 0;
+            }
+
+            lfs_size_t noff = size - 1;
+            lfs_off_t index = lfs_ctz_index(lfs, &noff);
+            noff = noff + 1;
+
+            // just copy out the last block if it is incomplete
+            if (noff != lfs->cfg->block_size) {
+                for (lfs_off_t i = 0; i < noff; i++) {
+                    uint8_t data;
+                    err = lfs_bd_read(lfs,
+                            NULL, rcache, noff-i,
+                            head, i, &data, 1);
+                    if (err) {
+                        return err;
+                    }
+
+                    err = lfs_bd_prog(lfs,
+                            pcache, rcache, true,
+                            nblock, i, &data, 1);
+                    if (err) {
+                        if (err == LFS_ERR_CORRUPT) {
+                            goto relocate;
+                        }
+                        return err;
+                    }
+                }
+
+                *block = nblock;
+                *off = noff;
+                return 0;
+            }
+
+            // append block
+            index += 1;
+            lfs_size_t skips = lfs_ctz(index) + 1;
+            lfs_block_t nhead = head;
+            for (lfs_off_t i = 0; i < skips; i++) {
+                nhead = lfs_tole32(nhead);
+                err = lfs_bd_prog(lfs, pcache, rcache, true,
+                        nblock, 4*i, &nhead, 4);
+                nhead = lfs_fromle32(nhead);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        goto relocate;
+                    }
+                    return err;
+                }
+
+                if (i != skips-1) {
+                    err = lfs_bd_read(lfs,
+                            NULL, rcache, sizeof(nhead),
+                            nhead, 4*i, &nhead, sizeof(nhead));
+                    nhead = lfs_fromle32(nhead);
+                    if (err) {
+                        return err;
+                    }
+                }
+            }
+
+            *block = nblock;
+            *off = 4*skips;
+            return 0;
+        }
+
+relocate:
+        LFS_DEBUG("Bad block at 0x%"PRIx32, nblock);
+
+        // just clear cache and try a new block
+        lfs_cache_drop(lfs, pcache);
+    }
+}
+
+static int lfs_ctz_traverse(lfs_t *lfs,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache,
+        lfs_block_t head, lfs_size_t size,
+        int (*cb)(void*, lfs_block_t), void *data) {
+    if (size == 0) {
+        return 0;
+    }
+
+    lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size-1});
+
+    while (true) {
+        int err = cb(data, head);
+        if (err) {
+            return err;
+        }
+
+        if (index == 0) {
+            return 0;
+        }
+
+        lfs_block_t heads[2];
+        int count = 2 - (index & 1);
+        err = lfs_bd_read(lfs,
+                pcache, rcache, count*sizeof(head),
+                head, 0, &heads, count*sizeof(head));
+        heads[0] = lfs_fromle32(heads[0]);
+        heads[1] = lfs_fromle32(heads[1]);
+        if (err) {
+            return err;
+        }
+
+        for (int i = 0; i < count-1; i++) {
+            err = cb(data, heads[i]);
+            if (err) {
+                return err;
+            }
+        }
+
+        head = heads[count-1];
+        index -= count;
+    }
+}
+
+
+/// Top level file operations ///
+int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file,
+        const char *path, int flags,
+        const struct lfs_file_config *cfg) {
+    LFS_TRACE("lfs_file_opencfg(%p, %p, \"%s\", %x, %p {"
+                 ".buffer=%p, .attrs=%p, .attr_count=%"PRIu32"})",
+            (void*)lfs, (void*)file, path, flags,
+            (void*)cfg, cfg->buffer, (void*)cfg->attrs, cfg->attr_count);
+
+    // deorphan if we haven't yet, needed at most once after poweron
+    if ((flags & 3) != LFS_O_RDONLY) {
+        int err = lfs_fs_forceconsistency(lfs);
+        if (err) {
+            LFS_TRACE("lfs_file_opencfg -> %d", err);
+            return err;
+        }
+    }
+
+    // setup simple file details
+    int err;
+    file->cfg = cfg;
+    file->flags = flags | LFS_F_OPENED;
+    file->pos = 0;
+    file->off = 0;
+    file->cache.buffer = NULL;
+
+    // allocate entry for file if it doesn't exist
+    lfs_stag_t tag = lfs_dir_find(lfs, &file->m, &path, &file->id);
+    if (tag < 0 && !(tag == LFS_ERR_NOENT && file->id != 0x3ff)) {
+        err = tag;
+        goto cleanup;
+    }
+
+    // get id, add to list of mdirs to catch update changes
+    file->type = LFS_TYPE_REG;
+    file->next = (lfs_file_t*)lfs->mlist;
+    lfs->mlist = (struct lfs_mlist*)file;
+
+    if (tag == LFS_ERR_NOENT) {
+        if (!(flags & LFS_O_CREAT)) {
+            err = LFS_ERR_NOENT;
+            goto cleanup;
+        }
+
+        // check that name fits
+        lfs_size_t nlen = strlen(path);
+        if (nlen > lfs->name_max) {
+            err = LFS_ERR_NAMETOOLONG;
+            goto cleanup;
+        }
+
+        // get next slot and create entry to remember name
+        err = lfs_dir_commit(lfs, &file->m, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_CREATE, file->id, 0), NULL},
+                {LFS_MKTAG(LFS_TYPE_REG, file->id, nlen), path},
+                {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), NULL}));
+        if (err) {
+            err = LFS_ERR_NAMETOOLONG;
+            goto cleanup;
+        }
+
+        tag = LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, 0);
+    } else if (flags & LFS_O_EXCL) {
+        err = LFS_ERR_EXIST;
+        goto cleanup;
+    } else if (lfs_tag_type3(tag) != LFS_TYPE_REG) {
+        err = LFS_ERR_ISDIR;
+        goto cleanup;
+    } else if (flags & LFS_O_TRUNC) {
+        // truncate if requested
+        tag = LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0);
+        file->flags |= LFS_F_DIRTY;
+    } else {
+        // try to load what's on disk, if it's inlined we'll fix it later
+        tag = lfs_dir_get(lfs, &file->m, LFS_MKTAG(0x700, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_STRUCT, file->id, 8), &file->ctz);
+        if (tag < 0) {
+            err = tag;
+            goto cleanup;
+        }
+        lfs_ctz_fromle32(&file->ctz);
+    }
+
+    // fetch attrs
+    for (unsigned i = 0; i < file->cfg->attr_count; i++) {
+        if ((file->flags & 3) != LFS_O_WRONLY) {
+            lfs_stag_t res = lfs_dir_get(lfs, &file->m,
+                    LFS_MKTAG(0x7ff, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_USERATTR + file->cfg->attrs[i].type,
+                        file->id, file->cfg->attrs[i].size),
+                        file->cfg->attrs[i].buffer);
+            if (res < 0 && res != LFS_ERR_NOENT) {
+                err = res;
+                goto cleanup;
+            }
+        }
+
+        if ((file->flags & 3) != LFS_O_RDONLY) {
+            if (file->cfg->attrs[i].size > lfs->attr_max) {
+                err = LFS_ERR_NOSPC;
+                goto cleanup;
+            }
+
+            file->flags |= LFS_F_DIRTY;
+        }
+    }
+
+    // allocate buffer if needed
+    if (file->cfg->buffer) {
+        file->cache.buffer = file->cfg->buffer;
+    } else {
+        file->cache.buffer = lfs_malloc(lfs->cfg->cache_size);
+        if (!file->cache.buffer) {
+            err = LFS_ERR_NOMEM;
+            goto cleanup;
+        }
+    }
+
+    // zero to avoid information leak
+    lfs_cache_zero(lfs, &file->cache);
+
+    if (lfs_tag_type3(tag) == LFS_TYPE_INLINESTRUCT) {
+        // load inline files
+        file->ctz.head = LFS_BLOCK_INLINE;
+        file->ctz.size = lfs_tag_size(tag);
+        file->flags |= LFS_F_INLINE;
+        file->cache.block = file->ctz.head;
+        file->cache.off = 0;
+        file->cache.size = lfs->cfg->cache_size;
+
+        // don't always read (may be new/trunc file)
+        if (file->ctz.size > 0) {
+            lfs_stag_t res = lfs_dir_get(lfs, &file->m,
+                    LFS_MKTAG(0x700, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_STRUCT, file->id,
+                        lfs_min(file->cache.size, 0x3fe)),
+                    file->cache.buffer);
+            if (res < 0) {
+                err = res;
+                goto cleanup;
+            }
+        }
+    }
+
+    LFS_TRACE("lfs_file_opencfg -> %d", 0);
+    return 0;
+
+cleanup:
+    // clean up lingering resources
+    file->flags |= LFS_F_ERRED;
+    lfs_file_close(lfs, file);
+    LFS_TRACE("lfs_file_opencfg -> %d", err);
+    return err;
+}
+
+int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
+        const char *path, int flags) {
+    LFS_TRACE("lfs_file_open(%p, %p, \"%s\", %x)",
+            (void*)lfs, (void*)file, path, flags);
+    static const struct lfs_file_config defaults = {0};
+    int err = lfs_file_opencfg(lfs, file, path, flags, &defaults);
+    LFS_TRACE("lfs_file_open -> %d", err);
+    return err;
+}
+
+int lfs_file_close(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_close(%p, %p)", (void*)lfs, (void*)file);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    int err = lfs_file_sync(lfs, file);
+
+    // remove from list of mdirs
+    for (struct lfs_mlist **p = &lfs->mlist; *p; p = &(*p)->next) {
+        if (*p == (struct lfs_mlist*)file) {
+            *p = (*p)->next;
+            break;
+        }
+    }
+
+    // clean up memory
+    if (!file->cfg->buffer) {
+        lfs_free(file->cache.buffer);
+    }
+
+    file->flags &= ~LFS_F_OPENED;
+    LFS_TRACE("lfs_file_close -> %d", err);
+    return err;
+}
+
+static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) {
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    while (true) {
+        // just relocate what exists into new block
+        lfs_block_t nblock;
+        int err = lfs_alloc(lfs, &nblock);
+        if (err) {
+            return err;
+        }
+
+        err = lfs_bd_erase(lfs, nblock);
+        if (err) {
+            if (err == LFS_ERR_CORRUPT) {
+                goto relocate;
+            }
+            return err;
+        }
+
+        // either read from dirty cache or disk
+        for (lfs_off_t i = 0; i < file->off; i++) {
+            uint8_t data;
+            if (file->flags & LFS_F_INLINE) {
+                err = lfs_dir_getread(lfs, &file->m,
+                        // note we evict inline files before they can be dirty
+                        NULL, &file->cache, file->off-i,
+                        LFS_MKTAG(0xfff, 0x1ff, 0),
+                        LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0),
+                        i, &data, 1);
+                if (err) {
+                    return err;
+                }
+            } else {
+                err = lfs_bd_read(lfs,
+                        &file->cache, &lfs->rcache, file->off-i,
+                        file->block, i, &data, 1);
+                if (err) {
+                    return err;
+                }
+            }
+
+            err = lfs_bd_prog(lfs,
+                    &lfs->pcache, &lfs->rcache, true,
+                    nblock, i, &data, 1);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+        }
+
+        // copy over new state of file
+        memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->cache_size);
+        file->cache.block = lfs->pcache.block;
+        file->cache.off = lfs->pcache.off;
+        file->cache.size = lfs->pcache.size;
+        lfs_cache_zero(lfs, &lfs->pcache);
+
+        file->block = nblock;
+        file->flags |= LFS_F_WRITING;
+        return 0;
+
+relocate:
+        LFS_DEBUG("Bad block at 0x%"PRIx32, nblock);
+
+        // just clear cache and try a new block
+        lfs_cache_drop(lfs, &lfs->pcache);
+    }
+}
+
+static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file) {
+    file->off = file->pos;
+    lfs_alloc_ack(lfs);
+    int err = lfs_file_relocate(lfs, file);
+    if (err) {
+        return err;
+    }
+
+    file->flags &= ~LFS_F_INLINE;
+    return 0;
+}
+
+static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) {
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    if (file->flags & LFS_F_READING) {
+        if (!(file->flags & LFS_F_INLINE)) {
+            lfs_cache_drop(lfs, &file->cache);
+        }
+        file->flags &= ~LFS_F_READING;
+    }
+
+    if (file->flags & LFS_F_WRITING) {
+        lfs_off_t pos = file->pos;
+
+        if (!(file->flags & LFS_F_INLINE)) {
+            // copy over anything after current branch
+            lfs_file_t orig = {
+                .ctz.head = file->ctz.head,
+                .ctz.size = file->ctz.size,
+                .flags = LFS_O_RDONLY | LFS_F_OPENED,
+                .pos = file->pos,
+                .cache = lfs->rcache,
+            };
+            lfs_cache_drop(lfs, &lfs->rcache);
+
+            while (file->pos < file->ctz.size) {
+                // copy over a byte at a time, leave it up to caching
+                // to make this efficient
+                uint8_t data;
+                lfs_ssize_t res = lfs_file_read(lfs, &orig, &data, 1);
+                if (res < 0) {
+                    return res;
+                }
+
+                res = lfs_file_write(lfs, file, &data, 1);
+                if (res < 0) {
+                    return res;
+                }
+
+                // keep our reference to the rcache in sync
+                if (lfs->rcache.block != LFS_BLOCK_NULL) {
+                    lfs_cache_drop(lfs, &orig.cache);
+                    lfs_cache_drop(lfs, &lfs->rcache);
+                }
+            }
+
+            // write out what we have
+            while (true) {
+                int err = lfs_bd_flush(lfs, &file->cache, &lfs->rcache, true);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        goto relocate;
+                    }
+                    return err;
+                }
+
+                break;
+
+relocate:
+                LFS_DEBUG("Bad block at 0x%"PRIx32, file->block);
+                err = lfs_file_relocate(lfs, file);
+                if (err) {
+                    return err;
+                }
+            }
+        } else {
+            file->pos = lfs_max(file->pos, file->ctz.size);
+        }
+
+        // actual file updates
+        file->ctz.head = file->block;
+        file->ctz.size = file->pos;
+        file->flags &= ~LFS_F_WRITING;
+        file->flags |= LFS_F_DIRTY;
+
+        file->pos = pos;
+    }
+
+    return 0;
+}
+
+int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_sync(%p, %p)", (void*)lfs, (void*)file);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    if (file->flags & LFS_F_ERRED) {
+        // it's not safe to do anything if our file errored
+        LFS_TRACE("lfs_file_sync -> %d", 0);
+        return 0;
+    }
+
+    int err = lfs_file_flush(lfs, file);
+    if (err) {
+        file->flags |= LFS_F_ERRED;
+        LFS_TRACE("lfs_file_sync -> %d", err);
+        return err;
+    }
+
+    if ((file->flags & LFS_F_DIRTY) &&
+            !lfs_pair_isnull(file->m.pair)) {
+        // update dir entry
+        uint16_t type;
+        const void *buffer;
+        lfs_size_t size;
+        struct lfs_ctz ctz;
+        if (file->flags & LFS_F_INLINE) {
+            // inline the whole file
+            type = LFS_TYPE_INLINESTRUCT;
+            buffer = file->cache.buffer;
+            size = file->ctz.size;
+        } else {
+            // update the ctz reference
+            type = LFS_TYPE_CTZSTRUCT;
+            // copy ctz so alloc will work during a relocate
+            ctz = file->ctz;
+            lfs_ctz_tole32(&ctz);
+            buffer = &ctz;
+            size = sizeof(ctz);
+        }
+
+        // commit file data and attributes
+        err = lfs_dir_commit(lfs, &file->m, LFS_MKATTRS(
+                {LFS_MKTAG(type, file->id, size), buffer},
+                {LFS_MKTAG(LFS_FROM_USERATTRS, file->id,
+                    file->cfg->attr_count), file->cfg->attrs}));
+        if (err) {
+            file->flags |= LFS_F_ERRED;
+            LFS_TRACE("lfs_file_sync -> %d", err);
+            return err;
+        }
+
+        file->flags &= ~LFS_F_DIRTY;
+    }
+
+    LFS_TRACE("lfs_file_sync -> %d", 0);
+    return 0;
+}
+
+lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file,
+        void *buffer, lfs_size_t size) {
+    LFS_TRACE("lfs_file_read(%p, %p, %p, %"PRIu32")",
+            (void*)lfs, (void*)file, buffer, size);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    LFS_ASSERT((file->flags & 3) != LFS_O_WRONLY);
+
+    uint8_t *data = buffer;
+    lfs_size_t nsize = size;
+
+    if (file->flags & LFS_F_WRITING) {
+        // flush out any writes
+        int err = lfs_file_flush(lfs, file);
+        if (err) {
+            LFS_TRACE("lfs_file_read -> %d", err);
+            return err;
+        }
+    }
+
+    if (file->pos >= file->ctz.size) {
+        // eof if past end
+        LFS_TRACE("lfs_file_read -> %d", 0);
+        return 0;
+    }
+
+    size = lfs_min(size, file->ctz.size - file->pos);
+    nsize = size;
+
+    while (nsize > 0) {
+        // check if we need a new block
+        if (!(file->flags & LFS_F_READING) ||
+                file->off == lfs->cfg->block_size) {
+            if (!(file->flags & LFS_F_INLINE)) {
+                int err = lfs_ctz_find(lfs, NULL, &file->cache,
+                        file->ctz.head, file->ctz.size,
+                        file->pos, &file->block, &file->off);
+                if (err) {
+                    LFS_TRACE("lfs_file_read -> %d", err);
+                    return err;
+                }
+            } else {
+                file->block = LFS_BLOCK_INLINE;
+                file->off = file->pos;
+            }
+
+            file->flags |= LFS_F_READING;
+        }
+
+        // read as much as we can in current block
+        lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off);
+        if (file->flags & LFS_F_INLINE) {
+            int err = lfs_dir_getread(lfs, &file->m,
+                    NULL, &file->cache, lfs->cfg->block_size,
+                    LFS_MKTAG(0xfff, 0x1ff, 0),
+                    LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0),
+                    file->off, data, diff);
+            if (err) {
+                LFS_TRACE("lfs_file_read -> %d", err);
+                return err;
+            }
+        } else {
+            int err = lfs_bd_read(lfs,
+                    NULL, &file->cache, lfs->cfg->block_size,
+                    file->block, file->off, data, diff);
+            if (err) {
+                LFS_TRACE("lfs_file_read -> %d", err);
+                return err;
+            }
+        }
+
+        file->pos += diff;
+        file->off += diff;
+        data += diff;
+        nsize -= diff;
+    }
+
+    LFS_TRACE("lfs_file_read -> %"PRId32, size);
+    return size;
+}
+
+lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
+        const void *buffer, lfs_size_t size) {
+    LFS_TRACE("lfs_file_write(%p, %p, %p, %"PRIu32")",
+            (void*)lfs, (void*)file, buffer, size);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    LFS_ASSERT((file->flags & 3) != LFS_O_RDONLY);
+
+    const uint8_t *data = buffer;
+    lfs_size_t nsize = size;
+
+    if (file->flags & LFS_F_READING) {
+        // drop any reads
+        int err = lfs_file_flush(lfs, file);
+        if (err) {
+            LFS_TRACE("lfs_file_write -> %d", err);
+            return err;
+        }
+    }
+
+    if ((file->flags & LFS_O_APPEND) && file->pos < file->ctz.size) {
+        file->pos = file->ctz.size;
+    }
+
+    if (file->pos + size > lfs->file_max) {
+        // Larger than file limit?
+        LFS_TRACE("lfs_file_write -> %d", LFS_ERR_FBIG);
+        return LFS_ERR_FBIG;
+    }
+
+    if (!(file->flags & LFS_F_WRITING) && file->pos > file->ctz.size) {
+        // fill with zeros
+        lfs_off_t pos = file->pos;
+        file->pos = file->ctz.size;
+
+        while (file->pos < pos) {
+            lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1);
+            if (res < 0) {
+                LFS_TRACE("lfs_file_write -> %"PRId32, res);
+                return res;
+            }
+        }
+    }
+
+    if ((file->flags & LFS_F_INLINE) &&
+            lfs_max(file->pos+nsize, file->ctz.size) >
+            lfs_min(0x3fe, lfs_min(
+                lfs->cfg->cache_size, lfs->cfg->block_size/8))) {
+        // inline file doesn't fit anymore
+        int err = lfs_file_outline(lfs, file);
+        if (err) {
+            file->flags |= LFS_F_ERRED;
+            LFS_TRACE("lfs_file_write -> %d", err);
+            return err;
+        }
+    }
+
+    while (nsize > 0) {
+        // check if we need a new block
+        if (!(file->flags & LFS_F_WRITING) ||
+                file->off == lfs->cfg->block_size) {
+            if (!(file->flags & LFS_F_INLINE)) {
+                if (!(file->flags & LFS_F_WRITING) && file->pos > 0) {
+                    // find out which block we're extending from
+                    int err = lfs_ctz_find(lfs, NULL, &file->cache,
+                            file->ctz.head, file->ctz.size,
+                            file->pos-1, &file->block, &file->off);
+                    if (err) {
+                        file->flags |= LFS_F_ERRED;
+                        LFS_TRACE("lfs_file_write -> %d", err);
+                        return err;
+                    }
+
+                    // mark cache as dirty since we may have read data into it
+                    lfs_cache_zero(lfs, &file->cache);
+                }
+
+                // extend file with new blocks
+                lfs_alloc_ack(lfs);
+                int err = lfs_ctz_extend(lfs, &file->cache, &lfs->rcache,
+                        file->block, file->pos,
+                        &file->block, &file->off);
+                if (err) {
+                    file->flags |= LFS_F_ERRED;
+                    LFS_TRACE("lfs_file_write -> %d", err);
+                    return err;
+                }
+            } else {
+                file->block = LFS_BLOCK_INLINE;
+                file->off = file->pos;
+            }
+
+            file->flags |= LFS_F_WRITING;
+        }
+
+        // program as much as we can in current block
+        lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off);
+        while (true) {
+            int err = lfs_bd_prog(lfs, &file->cache, &lfs->rcache, true,
+                    file->block, file->off, data, diff);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                file->flags |= LFS_F_ERRED;
+                LFS_TRACE("lfs_file_write -> %d", err);
+                return err;
+            }
+
+            break;
+relocate:
+            err = lfs_file_relocate(lfs, file);
+            if (err) {
+                file->flags |= LFS_F_ERRED;
+                LFS_TRACE("lfs_file_write -> %d", err);
+                return err;
+            }
+        }
+
+        file->pos += diff;
+        file->off += diff;
+        data += diff;
+        nsize -= diff;
+
+        lfs_alloc_ack(lfs);
+    }
+
+    file->flags &= ~LFS_F_ERRED;
+    LFS_TRACE("lfs_file_write -> %"PRId32, size);
+    return size;
+}
+
+lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file,
+        lfs_soff_t off, int whence) {
+    LFS_TRACE("lfs_file_seek(%p, %p, %"PRId32", %d)",
+            (void*)lfs, (void*)file, off, whence);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    // write out everything beforehand, may be noop if rdonly
+    int err = lfs_file_flush(lfs, file);
+    if (err) {
+        LFS_TRACE("lfs_file_seek -> %d", err);
+        return err;
+    }
+
+    // find new pos
+    lfs_off_t npos = file->pos;
+    if (whence == LFS_SEEK_SET) {
+        npos = off;
+    } else if (whence == LFS_SEEK_CUR) {
+        npos = file->pos + off;
+    } else if (whence == LFS_SEEK_END) {
+        npos = file->ctz.size + off;
+    }
+
+    if (npos > lfs->file_max) {
+        // file position out of range
+        LFS_TRACE("lfs_file_seek -> %d", LFS_ERR_INVAL);
+        return LFS_ERR_INVAL;
+    }
+
+    // update pos
+    file->pos = npos;
+    LFS_TRACE("lfs_file_seek -> %"PRId32, npos);
+    return npos;
+}
+
+int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) {
+    LFS_TRACE("lfs_file_truncate(%p, %p, %"PRIu32")",
+            (void*)lfs, (void*)file, size);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    LFS_ASSERT((file->flags & 3) != LFS_O_RDONLY);
+
+    if (size > LFS_FILE_MAX) {
+        LFS_TRACE("lfs_file_truncate -> %d", LFS_ERR_INVAL);
+        return LFS_ERR_INVAL;
+    }
+
+    lfs_off_t pos = file->pos;
+    lfs_off_t oldsize = lfs_file_size(lfs, file);
+    if (size < oldsize) {
+        // need to flush since directly changing metadata
+        int err = lfs_file_flush(lfs, file);
+        if (err) {
+            LFS_TRACE("lfs_file_truncate -> %d", err);
+            return err;
+        }
+
+        // lookup new head in ctz skip list
+        err = lfs_ctz_find(lfs, NULL, &file->cache,
+                file->ctz.head, file->ctz.size,
+                size, &file->block, &file->off);
+        if (err) {
+            LFS_TRACE("lfs_file_truncate -> %d", err);
+            return err;
+        }
+
+        file->ctz.head = file->block;
+        file->ctz.size = size;
+        file->flags |= LFS_F_DIRTY | LFS_F_READING;
+    } else if (size > oldsize) {
+        // flush+seek if not already at end
+        if (file->pos != oldsize) {
+            lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_END);
+            if (res < 0) {
+                LFS_TRACE("lfs_file_truncate -> %"PRId32, res);
+                return (int)res;
+            }
+        }
+
+        // fill with zeros
+        while (file->pos < size) {
+            lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1);
+            if (res < 0) {
+                LFS_TRACE("lfs_file_truncate -> %"PRId32, res);
+                return (int)res;
+            }
+        }
+    }
+
+    // restore pos
+    lfs_soff_t res = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET);
+    if (res < 0) {
+      LFS_TRACE("lfs_file_truncate -> %"PRId32, res);
+      return (int)res;
+    }
+
+    LFS_TRACE("lfs_file_truncate -> %d", 0);
+    return 0;
+}
+
+lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_tell(%p, %p)", (void*)lfs, (void*)file);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    (void)lfs;
+    LFS_TRACE("lfs_file_tell -> %"PRId32, file->pos);
+    return file->pos;
+}
+
+int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_rewind(%p, %p)", (void*)lfs, (void*)file);
+    lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_SET);
+    if (res < 0) {
+        LFS_TRACE("lfs_file_rewind -> %"PRId32, res);
+        return (int)res;
+    }
+
+    LFS_TRACE("lfs_file_rewind -> %d", 0);
+    return 0;
+}
+
+lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_size(%p, %p)", (void*)lfs, (void*)file);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    (void)lfs;
+    if (file->flags & LFS_F_WRITING) {
+        LFS_TRACE("lfs_file_size -> %"PRId32,
+                lfs_max(file->pos, file->ctz.size));
+        return lfs_max(file->pos, file->ctz.size);
+    } else {
+        LFS_TRACE("lfs_file_size -> %"PRId32, file->ctz.size);
+        return file->ctz.size;
+    }
+}
+
+
+/// General fs operations ///
+int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) {
+    LFS_TRACE("lfs_stat(%p, \"%s\", %p)", (void*)lfs, path, (void*)info);
+    lfs_mdir_t cwd;
+    lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL);
+    if (tag < 0) {
+        LFS_TRACE("lfs_stat -> %"PRId32, tag);
+        return (int)tag;
+    }
+
+    int err = lfs_dir_getinfo(lfs, &cwd, lfs_tag_id(tag), info);
+    LFS_TRACE("lfs_stat -> %d", err);
+    return err;
+}
+
+int lfs_remove(lfs_t *lfs, const char *path) {
+    LFS_TRACE("lfs_remove(%p, \"%s\")", (void*)lfs, path);
+    // deorphan if we haven't yet, needed at most once after poweron
+    int err = lfs_fs_forceconsistency(lfs);
+    if (err) {
+        LFS_TRACE("lfs_remove -> %d", err);
+        return err;
+    }
+
+    lfs_mdir_t cwd;
+    lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL);
+    if (tag < 0 || lfs_tag_id(tag) == 0x3ff) {
+        LFS_TRACE("lfs_remove -> %"PRId32, (tag < 0) ? tag : LFS_ERR_INVAL);
+        return (tag < 0) ? (int)tag : LFS_ERR_INVAL;
+    }
+
+    struct lfs_mlist dir;
+    dir.next = lfs->mlist;
+    if (lfs_tag_type3(tag) == LFS_TYPE_DIR) {
+        // must be empty before removal
+        lfs_block_t pair[2];
+        lfs_stag_t res = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x700, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair);
+        if (res < 0) {
+            LFS_TRACE("lfs_remove -> %"PRId32, res);
+            return (int)res;
+        }
+        lfs_pair_fromle32(pair);
+
+        err = lfs_dir_fetch(lfs, &dir.m, pair);
+        if (err) {
+            LFS_TRACE("lfs_remove -> %d", err);
+            return err;
+        }
+
+        if (dir.m.count > 0 || dir.m.split) {
+            LFS_TRACE("lfs_remove -> %d", LFS_ERR_NOTEMPTY);
+            return LFS_ERR_NOTEMPTY;
+        }
+
+        // mark fs as orphaned
+        lfs_fs_preporphans(lfs, +1);
+
+        // I know it's crazy but yes, dir can be changed by our parent's
+        // commit (if predecessor is child)
+        dir.type = 0;
+        dir.id = 0;
+        lfs->mlist = &dir;
+    }
+
+    // delete the entry
+    err = lfs_dir_commit(lfs, &cwd, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_DELETE, lfs_tag_id(tag), 0), NULL}));
+    if (err) {
+        lfs->mlist = dir.next;
+        LFS_TRACE("lfs_remove -> %d", err);
+        return err;
+    }
+
+    lfs->mlist = dir.next;
+    if (lfs_tag_type3(tag) == LFS_TYPE_DIR) {
+        // fix orphan
+        lfs_fs_preporphans(lfs, -1);
+
+        err = lfs_fs_pred(lfs, dir.m.pair, &cwd);
+        if (err) {
+            LFS_TRACE("lfs_remove -> %d", err);
+            return err;
+        }
+
+        err = lfs_dir_drop(lfs, &cwd, &dir.m);
+        if (err) {
+            LFS_TRACE("lfs_remove -> %d", err);
+            return err;
+        }
+    }
+
+    LFS_TRACE("lfs_remove -> %d", 0);
+    return 0;
+}
+
+int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
+    LFS_TRACE("lfs_rename(%p, \"%s\", \"%s\")", (void*)lfs, oldpath, newpath);
+
+    // deorphan if we haven't yet, needed at most once after poweron
+    int err = lfs_fs_forceconsistency(lfs);
+    if (err) {
+        LFS_TRACE("lfs_rename -> %d", err);
+        return err;
+    }
+
+    // find old entry
+    lfs_mdir_t oldcwd;
+    lfs_stag_t oldtag = lfs_dir_find(lfs, &oldcwd, &oldpath, NULL);
+    if (oldtag < 0 || lfs_tag_id(oldtag) == 0x3ff) {
+        LFS_TRACE("lfs_rename -> %"PRId32,
+                (oldtag < 0) ? oldtag : LFS_ERR_INVAL);
+        return (oldtag < 0) ? (int)oldtag : LFS_ERR_INVAL;
+    }
+
+    // find new entry
+    lfs_mdir_t newcwd;
+    uint16_t newid;
+    lfs_stag_t prevtag = lfs_dir_find(lfs, &newcwd, &newpath, &newid);
+    if ((prevtag < 0 || lfs_tag_id(prevtag) == 0x3ff) &&
+            !(prevtag == LFS_ERR_NOENT && newid != 0x3ff)) {
+        LFS_TRACE("lfs_rename -> %"PRId32,
+            (prevtag < 0) ? prevtag : LFS_ERR_INVAL);
+        return (prevtag < 0) ? (int)prevtag : LFS_ERR_INVAL;
+    }
+
+    // if we're in the same pair there's a few special cases...
+    bool samepair = (lfs_pair_cmp(oldcwd.pair, newcwd.pair) == 0);
+    uint16_t newoldid = lfs_tag_id(oldtag);
+
+    struct lfs_mlist prevdir;
+    prevdir.next = lfs->mlist;
+    if (prevtag == LFS_ERR_NOENT) {
+        // check that name fits
+        lfs_size_t nlen = strlen(newpath);
+        if (nlen > lfs->name_max) {
+            LFS_TRACE("lfs_rename -> %d", LFS_ERR_NAMETOOLONG);
+            return LFS_ERR_NAMETOOLONG;
+        }
+
+        // there is a small chance we are being renamed in the same
+        // directory/ to an id less than our old id, the global update
+        // to handle this is a bit messy
+        if (samepair && newid <= newoldid) {
+            newoldid += 1;
+        }
+    } else if (lfs_tag_type3(prevtag) != lfs_tag_type3(oldtag)) {
+        LFS_TRACE("lfs_rename -> %d", LFS_ERR_ISDIR);
+        return LFS_ERR_ISDIR;
+    } else if (samepair && newid == newoldid) {
+        // we're renaming to ourselves??
+        LFS_TRACE("lfs_rename -> %d", 0);
+        return 0;
+    } else if (lfs_tag_type3(prevtag) == LFS_TYPE_DIR) {
+        // must be empty before removal
+        lfs_block_t prevpair[2];
+        lfs_stag_t res = lfs_dir_get(lfs, &newcwd, LFS_MKTAG(0x700, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_STRUCT, newid, 8), prevpair);
+        if (res < 0) {
+            LFS_TRACE("lfs_rename -> %"PRId32, res);
+            return (int)res;
+        }
+        lfs_pair_fromle32(prevpair);
+
+        // must be empty before removal
+        err = lfs_dir_fetch(lfs, &prevdir.m, prevpair);
+        if (err) {
+            LFS_TRACE("lfs_rename -> %d", err);
+            return err;
+        }
+
+        if (prevdir.m.count > 0 || prevdir.m.split) {
+            LFS_TRACE("lfs_rename -> %d", LFS_ERR_NOTEMPTY);
+            return LFS_ERR_NOTEMPTY;
+        }
+
+        // mark fs as orphaned
+        lfs_fs_preporphans(lfs, +1);
+
+        // I know it's crazy but yes, dir can be changed by our parent's
+        // commit (if predecessor is child)
+        prevdir.type = 0;
+        prevdir.id = 0;
+        lfs->mlist = &prevdir;
+    }
+
+    if (!samepair) {
+        lfs_fs_prepmove(lfs, newoldid, oldcwd.pair);
+    }
+
+    // move over all attributes
+    err = lfs_dir_commit(lfs, &newcwd, LFS_MKATTRS(
+            {LFS_MKTAG_IF(prevtag != LFS_ERR_NOENT,
+                LFS_TYPE_DELETE, newid, 0), NULL},
+            {LFS_MKTAG(LFS_TYPE_CREATE, newid, 0), NULL},
+            {LFS_MKTAG(lfs_tag_type3(oldtag), newid, strlen(newpath)), newpath},
+            {LFS_MKTAG(LFS_FROM_MOVE, newid, lfs_tag_id(oldtag)), &oldcwd},
+            {LFS_MKTAG_IF(samepair,
+                LFS_TYPE_DELETE, newoldid, 0), NULL}));
+    if (err) {
+        lfs->mlist = prevdir.next;
+        LFS_TRACE("lfs_rename -> %d", err);
+        return err;
+    }
+
+    // let commit clean up after move (if we're different! otherwise move
+    // logic already fixed it for us)
+    if (!samepair && lfs_gstate_hasmove(&lfs->gstate)) {
+        // prep gstate and delete move id
+        lfs_fs_prepmove(lfs, 0x3ff, NULL);
+        err = lfs_dir_commit(lfs, &oldcwd, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_DELETE, lfs_tag_id(oldtag), 0), NULL}));
+        if (err) {
+            lfs->mlist = prevdir.next;
+            LFS_TRACE("lfs_rename -> %d", err);
+            return err;
+        }
+    }
+
+    lfs->mlist = prevdir.next;
+    if (prevtag != LFS_ERR_NOENT && lfs_tag_type3(prevtag) == LFS_TYPE_DIR) {
+        // fix orphan
+        lfs_fs_preporphans(lfs, -1);
+
+        err = lfs_fs_pred(lfs, prevdir.m.pair, &newcwd);
+        if (err) {
+            LFS_TRACE("lfs_rename -> %d", err);
+            return err;
+        }
+
+        err = lfs_dir_drop(lfs, &newcwd, &prevdir.m);
+        if (err) {
+            LFS_TRACE("lfs_rename -> %d", err);
+            return err;
+        }
+    }
+
+    LFS_TRACE("lfs_rename -> %d", 0);
+    return 0;
+}
+
+lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path,
+        uint8_t type, void *buffer, lfs_size_t size) {
+    LFS_TRACE("lfs_getattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")",
+            (void*)lfs, path, type, buffer, size);
+    lfs_mdir_t cwd;
+    lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL);
+    if (tag < 0) {
+        LFS_TRACE("lfs_getattr -> %"PRId32, tag);
+        return tag;
+    }
+
+    uint16_t id = lfs_tag_id(tag);
+    if (id == 0x3ff) {
+        // special case for root
+        id = 0;
+        int err = lfs_dir_fetch(lfs, &cwd, lfs->root);
+        if (err) {
+            LFS_TRACE("lfs_getattr -> %d", err);
+            return err;
+        }
+    }
+
+    tag = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x7ff, 0x3ff, 0),
+            LFS_MKTAG(LFS_TYPE_USERATTR + type,
+                id, lfs_min(size, lfs->attr_max)),
+            buffer);
+    if (tag < 0) {
+        if (tag == LFS_ERR_NOENT) {
+            LFS_TRACE("lfs_getattr -> %d", LFS_ERR_NOATTR);
+            return LFS_ERR_NOATTR;
+        }
+
+        LFS_TRACE("lfs_getattr -> %"PRId32, tag);
+        return tag;
+    }
+
+    size = lfs_tag_size(tag);
+    LFS_TRACE("lfs_getattr -> %"PRId32, size);
+    return size;
+}
+
+static int lfs_commitattr(lfs_t *lfs, const char *path,
+        uint8_t type, const void *buffer, lfs_size_t size) {
+    lfs_mdir_t cwd;
+    lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL);
+    if (tag < 0) {
+        return tag;
+    }
+
+    uint16_t id = lfs_tag_id(tag);
+    if (id == 0x3ff) {
+        // special case for root
+        id = 0;
+        int err = lfs_dir_fetch(lfs, &cwd, lfs->root);
+        if (err) {
+            return err;
+        }
+    }
+
+    return lfs_dir_commit(lfs, &cwd, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_USERATTR + type, id, size), buffer}));
+}
+
+int lfs_setattr(lfs_t *lfs, const char *path,
+        uint8_t type, const void *buffer, lfs_size_t size) {
+    LFS_TRACE("lfs_setattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")",
+            (void*)lfs, path, type, buffer, size);
+    if (size > lfs->attr_max) {
+        LFS_TRACE("lfs_setattr -> %d", LFS_ERR_NOSPC);
+        return LFS_ERR_NOSPC;
+    }
+
+    int err = lfs_commitattr(lfs, path, type, buffer, size);
+    LFS_TRACE("lfs_setattr -> %d", err);
+    return err;
+}
+
+int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type) {
+    LFS_TRACE("lfs_removeattr(%p, \"%s\", %"PRIu8")", (void*)lfs, path, type);
+    int err = lfs_commitattr(lfs, path, type, NULL, 0x3ff);
+    LFS_TRACE("lfs_removeattr -> %d", err);
+    return err;
+}
+
+
+/// Filesystem operations ///
+static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
+    lfs->cfg = cfg;
+    int err = 0;
+
+    // validate that the lfs-cfg sizes were initiated properly before
+    // performing any arithmetic logics with them
+    LFS_ASSERT(lfs->cfg->read_size != 0);
+    LFS_ASSERT(lfs->cfg->prog_size != 0);
+    LFS_ASSERT(lfs->cfg->cache_size != 0);
+
+    // check that block size is a multiple of cache size is a multiple
+    // of prog and read sizes
+    LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->read_size == 0);
+    LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->prog_size == 0);
+    LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->cache_size == 0);
+
+    // check that the block size is large enough to fit ctz pointers
+    LFS_ASSERT(4*lfs_npw2(0xffffffff / (lfs->cfg->block_size-2*4))
+            <= lfs->cfg->block_size);
+
+    // block_cycles = 0 is no longer supported.
+    //
+    // block_cycles is the number of erase cycles before littlefs evicts
+    // metadata logs as a part of wear leveling. Suggested values are in the
+    // range of 100-1000, or set block_cycles to -1 to disable block-level
+    // wear-leveling.
+    LFS_ASSERT(lfs->cfg->block_cycles != 0);
+
+
+    // setup read cache
+    if (lfs->cfg->read_buffer) {
+        lfs->rcache.buffer = lfs->cfg->read_buffer;
+    } else {
+        lfs->rcache.buffer = lfs_malloc(lfs->cfg->cache_size);
+        if (!lfs->rcache.buffer) {
+            err = LFS_ERR_NOMEM;
+            goto cleanup;
+        }
+    }
+
+    // setup program cache
+    if (lfs->cfg->prog_buffer) {
+        lfs->pcache.buffer = lfs->cfg->prog_buffer;
+    } else {
+        lfs->pcache.buffer = lfs_malloc(lfs->cfg->cache_size);
+        if (!lfs->pcache.buffer) {
+            err = LFS_ERR_NOMEM;
+            goto cleanup;
+        }
+    }
+
+    // zero to avoid information leaks
+    lfs_cache_zero(lfs, &lfs->rcache);
+    lfs_cache_zero(lfs, &lfs->pcache);
+
+    // setup lookahead, must be multiple of 64-bits, 32-bit aligned
+    LFS_ASSERT(lfs->cfg->lookahead_size > 0);
+    LFS_ASSERT(lfs->cfg->lookahead_size % 8 == 0 &&
+            (uintptr_t)lfs->cfg->lookahead_buffer % 4 == 0);
+    if (lfs->cfg->lookahead_buffer) {
+        lfs->free.buffer = lfs->cfg->lookahead_buffer;
+    } else {
+        lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead_size);
+        if (!lfs->free.buffer) {
+            err = LFS_ERR_NOMEM;
+            goto cleanup;
+        }
+    }
+
+    // check that the size limits are sane
+    LFS_ASSERT(lfs->cfg->name_max <= LFS_NAME_MAX);
+    lfs->name_max = lfs->cfg->name_max;
+    if (!lfs->name_max) {
+        lfs->name_max = LFS_NAME_MAX;
+    }
+
+    LFS_ASSERT(lfs->cfg->file_max <= LFS_FILE_MAX);
+    lfs->file_max = lfs->cfg->file_max;
+    if (!lfs->file_max) {
+        lfs->file_max = LFS_FILE_MAX;
+    }
+
+    LFS_ASSERT(lfs->cfg->attr_max <= LFS_ATTR_MAX);
+    lfs->attr_max = lfs->cfg->attr_max;
+    if (!lfs->attr_max) {
+        lfs->attr_max = LFS_ATTR_MAX;
+    }
+
+    // setup default state
+    lfs->root[0] = LFS_BLOCK_NULL;
+    lfs->root[1] = LFS_BLOCK_NULL;
+    lfs->mlist = NULL;
+    lfs->seed = 0;
+    lfs->gdisk = (lfs_gstate_t){0};
+    lfs->gstate = (lfs_gstate_t){0};
+    lfs->gdelta = (lfs_gstate_t){0};
+#ifdef LFS_MIGRATE
+    lfs->lfs1 = NULL;
+#endif
+
+    return 0;
+
+cleanup:
+    lfs_deinit(lfs);
+    return err;
+}
+
+static int lfs_deinit(lfs_t *lfs) {
+    // free allocated memory
+    if (!lfs->cfg->read_buffer) {
+        lfs_free(lfs->rcache.buffer);
+    }
+
+    if (!lfs->cfg->prog_buffer) {
+        lfs_free(lfs->pcache.buffer);
+    }
+
+    if (!lfs->cfg->lookahead_buffer) {
+        lfs_free(lfs->free.buffer);
+    }
+
+    return 0;
+}
+
+int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
+    LFS_TRACE("lfs_format(%p, %p {.context=%p, "
+                ".read=%p, .prog=%p, .erase=%p, .sync=%p, "
+                ".read_size=%"PRIu32", .prog_size=%"PRIu32", "
+                ".block_size=%"PRIu32", .block_count=%"PRIu32", "
+                ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", "
+                ".lookahead_size=%"PRIu32", .read_buffer=%p, "
+                ".prog_buffer=%p, .lookahead_buffer=%p, "
+                ".name_max=%"PRIu32", .file_max=%"PRIu32", "
+                ".attr_max=%"PRIu32"})",
+            (void*)lfs, (void*)cfg, cfg->context,
+            (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog,
+            (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync,
+            cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count,
+            cfg->block_cycles, cfg->cache_size, cfg->lookahead_size,
+            cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer,
+            cfg->name_max, cfg->file_max, cfg->attr_max);
+    int err = 0;
+    {
+        err = lfs_init(lfs, cfg);
+        if (err) {
+            LFS_TRACE("lfs_format -> %d", err);
+            return err;
+        }
+
+        // create free lookahead
+        memset(lfs->free.buffer, 0, lfs->cfg->lookahead_size);
+        lfs->free.off = 0;
+        lfs->free.size = lfs_min(8*lfs->cfg->lookahead_size,
+                lfs->cfg->block_count);
+        lfs->free.i = 0;
+        lfs_alloc_ack(lfs);
+
+        // create root dir
+        lfs_mdir_t root;
+        err = lfs_dir_alloc(lfs, &root);
+        if (err) {
+            goto cleanup;
+        }
+
+        // write one superblock
+        lfs_superblock_t superblock = {
+            .version     = LFS_DISK_VERSION,
+            .block_size  = lfs->cfg->block_size,
+            .block_count = lfs->cfg->block_count,
+            .name_max    = lfs->name_max,
+            .file_max    = lfs->file_max,
+            .attr_max    = lfs->attr_max,
+        };
+
+        lfs_superblock_tole32(&superblock);
+        err = lfs_dir_commit(lfs, &root, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_CREATE, 0, 0), NULL},
+                {LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"},
+                {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
+                    &superblock}));
+        if (err) {
+            goto cleanup;
+        }
+
+        // sanity check that fetch works
+        err = lfs_dir_fetch(lfs, &root, (const lfs_block_t[2]){0, 1});
+        if (err) {
+            goto cleanup;
+        }
+
+        // force compaction to prevent accidentally mounting any
+        // older version of littlefs that may live on disk
+        root.erased = false;
+        err = lfs_dir_commit(lfs, &root, NULL, 0);
+        if (err) {
+            goto cleanup;
+        }
+    }
+
+cleanup:
+    lfs_deinit(lfs);
+    LFS_TRACE("lfs_format -> %d", err);
+    return err;
+}
+
+int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) {
+    LFS_TRACE("lfs_mount(%p, %p {.context=%p, "
+                ".read=%p, .prog=%p, .erase=%p, .sync=%p, "
+                ".read_size=%"PRIu32", .prog_size=%"PRIu32", "
+                ".block_size=%"PRIu32", .block_count=%"PRIu32", "
+                ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", "
+                ".lookahead_size=%"PRIu32", .read_buffer=%p, "
+                ".prog_buffer=%p, .lookahead_buffer=%p, "
+                ".name_max=%"PRIu32", .file_max=%"PRIu32", "
+                ".attr_max=%"PRIu32"})",
+            (void*)lfs, (void*)cfg, cfg->context,
+            (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog,
+            (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync,
+            cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count,
+            cfg->block_cycles, cfg->cache_size, cfg->lookahead_size,
+            cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer,
+            cfg->name_max, cfg->file_max, cfg->attr_max);
+    int err = lfs_init(lfs, cfg);
+    if (err) {
+        LFS_TRACE("lfs_mount -> %d", err);
+        return err;
+    }
+
+    // scan directory blocks for superblock and any global updates
+    lfs_mdir_t dir = {.tail = {0, 1}};
+    lfs_block_t cycle = 0;
+    while (!lfs_pair_isnull(dir.tail)) {
+        if (cycle >= lfs->cfg->block_count/2) {
+            // loop detected
+            err = LFS_ERR_CORRUPT;
+            goto cleanup;
+        }
+        cycle += 1;
+
+        // fetch next block in tail list
+        lfs_stag_t tag = lfs_dir_fetchmatch(lfs, &dir, dir.tail,
+                LFS_MKTAG(0x7ff, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8),
+                NULL,
+                lfs_dir_find_match, &(struct lfs_dir_find_match){
+                    lfs, "littlefs", 8});
+        if (tag < 0) {
+            err = tag;
+            goto cleanup;
+        }
+
+        // has superblock?
+        if (tag && !lfs_tag_isdelete(tag)) {
+            // update root
+            lfs->root[0] = dir.pair[0];
+            lfs->root[1] = dir.pair[1];
+
+            // grab superblock
+            lfs_superblock_t superblock;
+            tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x7ff, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
+                    &superblock);
+            if (tag < 0) {
+                err = tag;
+                goto cleanup;
+            }
+            lfs_superblock_fromle32(&superblock);
+
+            // check version
+            uint16_t major_version = (0xffff & (superblock.version >> 16));
+            uint16_t minor_version = (0xffff & (superblock.version >>  0));
+            if ((major_version != LFS_DISK_VERSION_MAJOR ||
+                 minor_version > LFS_DISK_VERSION_MINOR)) {
+                LFS_ERROR("Invalid version v%"PRIu16".%"PRIu16,
+                        major_version, minor_version);
+                err = LFS_ERR_INVAL;
+                goto cleanup;
+            }
+
+            // check superblock configuration
+            if (superblock.name_max) {
+                if (superblock.name_max > lfs->name_max) {
+                    LFS_ERROR("Unsupported name_max (%"PRIu32" > %"PRIu32")",
+                            superblock.name_max, lfs->name_max);
+                    err = LFS_ERR_INVAL;
+                    goto cleanup;
+                }
+
+                lfs->name_max = superblock.name_max;
+            }
+
+            if (superblock.file_max) {
+                if (superblock.file_max > lfs->file_max) {
+                    LFS_ERROR("Unsupported file_max (%"PRIu32" > %"PRIu32")",
+                            superblock.file_max, lfs->file_max);
+                    err = LFS_ERR_INVAL;
+                    goto cleanup;
+                }
+
+                lfs->file_max = superblock.file_max;
+            }
+
+            if (superblock.attr_max) {
+                if (superblock.attr_max > lfs->attr_max) {
+                    LFS_ERROR("Unsupported attr_max (%"PRIu32" > %"PRIu32")",
+                            superblock.attr_max, lfs->attr_max);
+                    err = LFS_ERR_INVAL;
+                    goto cleanup;
+                }
+
+                lfs->attr_max = superblock.attr_max;
+            }
+        }
+
+        // has gstate?
+        err = lfs_dir_getgstate(lfs, &dir, &lfs->gstate);
+        if (err) {
+            goto cleanup;
+        }
+    }
+
+    // found superblock?
+    if (lfs_pair_isnull(lfs->root)) {
+        err = LFS_ERR_INVAL;
+        goto cleanup;
+    }
+
+    // update littlefs with gstate
+    if (!lfs_gstate_iszero(&lfs->gstate)) {
+        LFS_DEBUG("Found pending gstate 0x%08"PRIx32"%08"PRIx32"%08"PRIx32,
+                lfs->gstate.tag,
+                lfs->gstate.pair[0],
+                lfs->gstate.pair[1]);
+    }
+    lfs->gstate.tag += !lfs_tag_isvalid(lfs->gstate.tag);
+    lfs->gdisk = lfs->gstate;
+
+    // setup free lookahead
+    lfs_alloc_reset(lfs);
+
+    LFS_TRACE("lfs_mount -> %d", 0);
+    return 0;
+
+cleanup:
+    lfs_unmount(lfs);
+    LFS_TRACE("lfs_mount -> %d", err);
+    return err;
+}
+
+int lfs_unmount(lfs_t *lfs) {
+    LFS_TRACE("lfs_unmount(%p)", (void*)lfs);
+    int err = lfs_deinit(lfs);
+    LFS_TRACE("lfs_unmount -> %d", err);
+    return err;
+}
+
+
+/// Filesystem filesystem operations ///
+int lfs_fs_traverseraw(lfs_t *lfs,
+        int (*cb)(void *data, lfs_block_t block), void *data,
+        bool includeorphans) {
+    // iterate over metadata pairs
+    lfs_mdir_t dir = {.tail = {0, 1}};
+
+#ifdef LFS_MIGRATE
+    // also consider v1 blocks during migration
+    if (lfs->lfs1) {
+        int err = lfs1_traverse(lfs, cb, data);
+        if (err) {
+            return err;
+        }
+
+        dir.tail[0] = lfs->root[0];
+        dir.tail[1] = lfs->root[1];
+    }
+#endif
+
+    lfs_block_t cycle = 0;
+    while (!lfs_pair_isnull(dir.tail)) {
+        if (cycle >= lfs->cfg->block_count/2) {
+            // loop detected
+            return LFS_ERR_CORRUPT;
+        }
+        cycle += 1;
+
+        for (int i = 0; i < 2; i++) {
+            int err = cb(data, dir.tail[i]);
+            if (err) {
+                return err;
+            }
+        }
+
+        // iterate through ids in directory
+        int err = lfs_dir_fetch(lfs, &dir, dir.tail);
+        if (err) {
+            return err;
+        }
+
+        for (uint16_t id = 0; id < dir.count; id++) {
+            struct lfs_ctz ctz;
+            lfs_stag_t tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x700, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_STRUCT, id, sizeof(ctz)), &ctz);
+            if (tag < 0) {
+                if (tag == LFS_ERR_NOENT) {
+                    continue;
+                }
+                return tag;
+            }
+            lfs_ctz_fromle32(&ctz);
+
+            if (lfs_tag_type3(tag) == LFS_TYPE_CTZSTRUCT) {
+                err = lfs_ctz_traverse(lfs, NULL, &lfs->rcache,
+                        ctz.head, ctz.size, cb, data);
+                if (err) {
+                    return err;
+                }
+            } else if (includeorphans && 
+                    lfs_tag_type3(tag) == LFS_TYPE_DIRSTRUCT) {
+                for (int i = 0; i < 2; i++) {
+                    err = cb(data, (&ctz.head)[i]);
+                    if (err) {
+                        return err;
+                    }
+                }
+            }
+        }
+    }
+
+    // iterate over any open files
+    for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) {
+        if (f->type != LFS_TYPE_REG) {
+            continue;
+        }
+
+        if ((f->flags & LFS_F_DIRTY) && !(f->flags & LFS_F_INLINE)) {
+            int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache,
+                    f->ctz.head, f->ctz.size, cb, data);
+            if (err) {
+                return err;
+            }
+        }
+
+        if ((f->flags & LFS_F_WRITING) && !(f->flags & LFS_F_INLINE)) {
+            int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache,
+                    f->block, f->pos, cb, data);
+            if (err) {
+                return err;
+            }
+        }
+    }
+
+    return 0;
+}
+
+int lfs_fs_traverse(lfs_t *lfs,
+        int (*cb)(void *data, lfs_block_t block), void *data) {
+    LFS_TRACE("lfs_fs_traverse(%p, %p, %p)",
+            (void*)lfs, (void*)(uintptr_t)cb, data);
+    int err = lfs_fs_traverseraw(lfs, cb, data, true);
+    LFS_TRACE("lfs_fs_traverse -> %d", 0);
+    return err;
+}
+
+static int lfs_fs_pred(lfs_t *lfs,
+        const lfs_block_t pair[2], lfs_mdir_t *pdir) {
+    // iterate over all directory directory entries
+    pdir->tail[0] = 0;
+    pdir->tail[1] = 1;
+    lfs_block_t cycle = 0;
+    while (!lfs_pair_isnull(pdir->tail)) {
+        if (cycle >= lfs->cfg->block_count/2) {
+            // loop detected
+            return LFS_ERR_CORRUPT;
+        }
+        cycle += 1;
+
+        if (lfs_pair_cmp(pdir->tail, pair) == 0) {
+            return 0;
+        }
+
+        int err = lfs_dir_fetch(lfs, pdir, pdir->tail);
+        if (err) {
+            return err;
+        }
+    }
+
+    return LFS_ERR_NOENT;
+}
+
+struct lfs_fs_parent_match {
+    lfs_t *lfs;
+    const lfs_block_t pair[2];
+};
+
+static int lfs_fs_parent_match(void *data,
+        lfs_tag_t tag, const void *buffer) {
+    struct lfs_fs_parent_match *find = data;
+    lfs_t *lfs = find->lfs;
+    const struct lfs_diskoff *disk = buffer;
+    (void)tag;
+
+    lfs_block_t child[2];
+    int err = lfs_bd_read(lfs,
+            &lfs->pcache, &lfs->rcache, lfs->cfg->block_size,
+            disk->block, disk->off, &child, sizeof(child));
+    if (err) {
+        return err;
+    }
+
+    lfs_pair_fromle32(child);
+    return (lfs_pair_cmp(child, find->pair) == 0) ? LFS_CMP_EQ : LFS_CMP_LT;
+}
+
+static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t pair[2],
+        lfs_mdir_t *parent) {
+    // use fetchmatch with callback to find pairs
+    parent->tail[0] = 0;
+    parent->tail[1] = 1;
+    lfs_block_t cycle = 0;
+    while (!lfs_pair_isnull(parent->tail)) {
+        if (cycle >= lfs->cfg->block_count/2) {
+            // loop detected
+            return LFS_ERR_CORRUPT;
+        }
+        cycle += 1;
+
+        lfs_stag_t tag = lfs_dir_fetchmatch(lfs, parent, parent->tail,
+                LFS_MKTAG(0x7ff, 0, 0x3ff),
+                LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 0, 8),
+                NULL,
+                lfs_fs_parent_match, &(struct lfs_fs_parent_match){
+                    lfs, {pair[0], pair[1]}});
+        if (tag && tag != LFS_ERR_NOENT) {
+            return tag;
+        }
+    }
+
+    return LFS_ERR_NOENT;
+}
+
+static int lfs_fs_relocate(lfs_t *lfs,
+        const lfs_block_t oldpair[2], lfs_block_t newpair[2]) {
+    // update internal root
+    if (lfs_pair_cmp(oldpair, lfs->root) == 0) {
+        lfs->root[0] = newpair[0];
+        lfs->root[1] = newpair[1];
+    }
+
+    // update internally tracked dirs
+    for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) {
+        if (lfs_pair_cmp(oldpair, d->m.pair) == 0) {
+            d->m.pair[0] = newpair[0];
+            d->m.pair[1] = newpair[1];
+        }
+
+        if (d->type == LFS_TYPE_DIR &&
+                lfs_pair_cmp(oldpair, ((lfs_dir_t*)d)->head) == 0) {
+            ((lfs_dir_t*)d)->head[0] = newpair[0];
+            ((lfs_dir_t*)d)->head[1] = newpair[1];
+        }
+    }
+
+    // find parent
+    lfs_mdir_t parent;
+    lfs_stag_t tag = lfs_fs_parent(lfs, oldpair, &parent);
+    if (tag < 0 && tag != LFS_ERR_NOENT) {
+        return tag;
+    }
+
+    if (tag != LFS_ERR_NOENT) {
+        // update disk, this creates a desync
+        lfs_fs_preporphans(lfs, +1);
+
+        // fix pending move in this pair? this looks like an optimization but
+        // is in fact _required_ since relocating may outdate the move.
+        uint16_t moveid = 0x3ff;
+        if (lfs_gstate_hasmovehere(&lfs->gstate, parent.pair)) {
+            moveid = lfs_tag_id(lfs->gstate.tag);
+            LFS_DEBUG("Fixing move while relocating "
+                    "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n",
+                    parent.pair[0], parent.pair[1], moveid);
+            lfs_fs_prepmove(lfs, 0x3ff, NULL);
+            if (moveid < lfs_tag_id(tag)) {
+                tag -= LFS_MKTAG(0, 1, 0);
+            }
+        }
+
+        lfs_pair_tole32(newpair);
+        int err = lfs_dir_commit(lfs, &parent, LFS_MKATTRS(
+                {LFS_MKTAG_IF(moveid != 0x3ff,
+                    LFS_TYPE_DELETE, moveid, 0), NULL},
+                {tag, newpair}));
+        lfs_pair_fromle32(newpair);
+        if (err) {
+            return err;
+        }
+
+        // next step, clean up orphans
+        lfs_fs_preporphans(lfs, -1);
+    }
+
+    // find pred
+    int err = lfs_fs_pred(lfs, oldpair, &parent);
+    if (err && err != LFS_ERR_NOENT) {
+        return err;
+    }
+
+    // if we can't find dir, it must be new
+    if (err != LFS_ERR_NOENT) {
+        // fix pending move in this pair? this looks like an optimization but
+        // is in fact _required_ since relocating may outdate the move.
+        uint16_t moveid = 0x3ff;
+        if (lfs_gstate_hasmovehere(&lfs->gstate, parent.pair)) {
+            moveid = lfs_tag_id(lfs->gstate.tag);
+            LFS_DEBUG("Fixing move while relocating "
+                    "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n",
+                    parent.pair[0], parent.pair[1], moveid);
+            lfs_fs_prepmove(lfs, 0x3ff, NULL);
+        }
+
+        // replace bad pair, either we clean up desync, or no desync occured
+        lfs_pair_tole32(newpair);
+        err = lfs_dir_commit(lfs, &parent, LFS_MKATTRS(
+                {LFS_MKTAG_IF(moveid != 0x3ff,
+                    LFS_TYPE_DELETE, moveid, 0), NULL},
+                {LFS_MKTAG(LFS_TYPE_TAIL + parent.split, 0x3ff, 8), newpair}));
+        lfs_pair_fromle32(newpair);
+        if (err) {
+            return err;
+        }
+    }
+
+    return 0;
+}
+
+static void lfs_fs_preporphans(lfs_t *lfs, int8_t orphans) {
+    LFS_ASSERT(lfs_tag_size(lfs->gstate.tag) > 0 || orphans >= 0);
+    lfs->gstate.tag += orphans;
+    lfs->gstate.tag = ((lfs->gstate.tag & ~LFS_MKTAG(0x800, 0, 0)) |
+            ((uint32_t)lfs_gstate_hasorphans(&lfs->gstate) << 31));
+}
+
+static void lfs_fs_prepmove(lfs_t *lfs,
+        uint16_t id, const lfs_block_t pair[2]) {
+    lfs->gstate.tag = ((lfs->gstate.tag & ~LFS_MKTAG(0x7ff, 0x3ff, 0)) |
+            ((id != 0x3ff) ? LFS_MKTAG(LFS_TYPE_DELETE, id, 0) : 0));
+    lfs->gstate.pair[0] = (id != 0x3ff) ? pair[0] : 0;
+    lfs->gstate.pair[1] = (id != 0x3ff) ? pair[1] : 0;
+}
+
+static int lfs_fs_demove(lfs_t *lfs) {
+    if (!lfs_gstate_hasmove(&lfs->gdisk)) {
+        return 0;
+    }
+
+    // Fix bad moves
+    LFS_DEBUG("Fixing move {0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16,
+            lfs->gdisk.pair[0],
+            lfs->gdisk.pair[1],
+            lfs_tag_id(lfs->gdisk.tag));
+
+    // fetch and delete the moved entry
+    lfs_mdir_t movedir;
+    int err = lfs_dir_fetch(lfs, &movedir, lfs->gdisk.pair);
+    if (err) {
+        return err;
+    }
+
+    // prep gstate and delete move id
+    uint16_t moveid = lfs_tag_id(lfs->gdisk.tag);
+    lfs_fs_prepmove(lfs, 0x3ff, NULL);
+    err = lfs_dir_commit(lfs, &movedir, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_DELETE, moveid, 0), NULL}));
+    if (err) {
+        return err;
+    }
+
+    return 0;
+}
+
+static int lfs_fs_deorphan(lfs_t *lfs) {
+    if (!lfs_gstate_hasorphans(&lfs->gstate)) {
+        return 0;
+    }
+
+    // Fix any orphans
+    lfs_mdir_t pdir = {.split = true, .tail = {0, 1}};
+    lfs_mdir_t dir;
+
+    // iterate over all directory directory entries
+    while (!lfs_pair_isnull(pdir.tail)) {
+        int err = lfs_dir_fetch(lfs, &dir, pdir.tail);
+        if (err) {
+            return err;
+        }
+
+        // check head blocks for orphans
+        if (!pdir.split) {
+            // check if we have a parent
+            lfs_mdir_t parent;
+            lfs_stag_t tag = lfs_fs_parent(lfs, pdir.tail, &parent);
+            if (tag < 0 && tag != LFS_ERR_NOENT) {
+                return tag;
+            }
+
+            if (tag == LFS_ERR_NOENT) {
+                // we are an orphan
+                LFS_DEBUG("Fixing orphan {0x%"PRIx32", 0x%"PRIx32"}",
+                        pdir.tail[0], pdir.tail[1]);
+
+                err = lfs_dir_drop(lfs, &pdir, &dir);
+                if (err) {
+                    return err;
+                }
+
+                // refetch tail
+                continue;
+            }
+
+            lfs_block_t pair[2];
+            lfs_stag_t res = lfs_dir_get(lfs, &parent,
+                    LFS_MKTAG(0x7ff, 0x3ff, 0), tag, pair);
+            if (res < 0) {
+                return res;
+            }
+            lfs_pair_fromle32(pair);
+
+            if (!lfs_pair_sync(pair, pdir.tail)) {
+                // we have desynced
+                LFS_DEBUG("Fixing half-orphan {0x%"PRIx32", 0x%"PRIx32"} "
+                            "-> {0x%"PRIx32", 0x%"PRIx32"}",
+                        pdir.tail[0], pdir.tail[1], pair[0], pair[1]);
+
+                lfs_pair_tole32(pair);
+                err = lfs_dir_commit(lfs, &pdir, LFS_MKATTRS(
+                        {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), pair}));
+                lfs_pair_fromle32(pair);
+                if (err) {
+                    return err;
+                }
+
+                // refetch tail
+                continue;
+            }
+        }
+
+        pdir = dir;
+    }
+
+    // mark orphans as fixed
+    lfs_fs_preporphans(lfs, -lfs_gstate_getorphans(&lfs->gstate));
+    return 0;
+}
+
+static int lfs_fs_forceconsistency(lfs_t *lfs) {
+    int err = lfs_fs_demove(lfs);
+    if (err) {
+        return err;
+    }
+
+    err = lfs_fs_deorphan(lfs);
+    if (err) {
+        return err;
+    }
+
+    return 0;
+}
+
+static int lfs_fs_size_count(void *p, lfs_block_t block) {
+    (void)block;
+    lfs_size_t *size = p;
+    *size += 1;
+    return 0;
+}
+
+lfs_ssize_t lfs_fs_size(lfs_t *lfs) {
+    LFS_TRACE("lfs_fs_size(%p)", (void*)lfs);
+    lfs_size_t size = 0;
+    int err = lfs_fs_traverseraw(lfs, lfs_fs_size_count, &size, false);
+    if (err) {
+        LFS_TRACE("lfs_fs_size -> %d", err);
+        return err;
+    }
+
+    LFS_TRACE("lfs_fs_size -> %d", err);
+    return size;
+}
+
+#ifdef LFS_MIGRATE
+////// Migration from littelfs v1 below this //////
+
+/// Version info ///
+
+// Software library version
+// Major (top-nibble), incremented on backwards incompatible changes
+// Minor (bottom-nibble), incremented on feature additions
+#define LFS1_VERSION 0x00010007
+#define LFS1_VERSION_MAJOR (0xffff & (LFS1_VERSION >> 16))
+#define LFS1_VERSION_MINOR (0xffff & (LFS1_VERSION >>  0))
+
+// Version of On-disk data structures
+// Major (top-nibble), incremented on backwards incompatible changes
+// Minor (bottom-nibble), incremented on feature additions
+#define LFS1_DISK_VERSION 0x00010001
+#define LFS1_DISK_VERSION_MAJOR (0xffff & (LFS1_DISK_VERSION >> 16))
+#define LFS1_DISK_VERSION_MINOR (0xffff & (LFS1_DISK_VERSION >>  0))
+
+
+/// v1 Definitions ///
+
+// File types
+enum lfs1_type {
+    LFS1_TYPE_REG        = 0x11,
+    LFS1_TYPE_DIR        = 0x22,
+    LFS1_TYPE_SUPERBLOCK = 0x2e,
+};
+
+typedef struct lfs1 {
+    lfs_block_t root[2];
+} lfs1_t;
+
+typedef struct lfs1_entry {
+    lfs_off_t off;
+
+    struct lfs1_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;
+} lfs1_entry_t;
+
+typedef struct lfs1_dir {
+    struct lfs1_dir *next;
+    lfs_block_t pair[2];
+    lfs_off_t off;
+
+    lfs_block_t head[2];
+    lfs_off_t pos;
+
+    struct lfs1_disk_dir {
+        uint32_t rev;
+        lfs_size_t size;
+        lfs_block_t tail[2];
+    } d;
+} lfs1_dir_t;
+
+typedef struct lfs1_superblock {
+    lfs_off_t off;
+
+    struct lfs1_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;
+} lfs1_superblock_t;
+
+
+/// Low-level wrappers v1->v2 ///
+static void lfs1_crc(uint32_t *crc, const void *buffer, size_t size) {
+    *crc = lfs_crc(*crc, buffer, size);
+}
+
+static int lfs1_bd_read(lfs_t *lfs, lfs_block_t block,
+        lfs_off_t off, void *buffer, lfs_size_t size) {
+    // if we ever do more than writes to alternating pairs,
+    // this may need to consider pcache
+    return lfs_bd_read(lfs, &lfs->pcache, &lfs->rcache, size,
+            block, off, buffer, size);
+}
+
+static int lfs1_bd_crc(lfs_t *lfs, lfs_block_t block,
+        lfs_off_t off, lfs_size_t size, uint32_t *crc) {
+    for (lfs_off_t i = 0; i < size; i++) {
+        uint8_t c;
+        int err = lfs1_bd_read(lfs, block, off+i, &c, 1);
+        if (err) {
+            return err;
+        }
+
+        lfs1_crc(crc, &c, 1);
+    }
+
+    return 0;
+}
+
+
+/// Endian swapping functions ///
+static void lfs1_dir_fromle32(struct lfs1_disk_dir *d) {
+    d->rev     = lfs_fromle32(d->rev);
+    d->size    = lfs_fromle32(d->size);
+    d->tail[0] = lfs_fromle32(d->tail[0]);
+    d->tail[1] = lfs_fromle32(d->tail[1]);
+}
+
+static void lfs1_dir_tole32(struct lfs1_disk_dir *d) {
+    d->rev     = lfs_tole32(d->rev);
+    d->size    = lfs_tole32(d->size);
+    d->tail[0] = lfs_tole32(d->tail[0]);
+    d->tail[1] = lfs_tole32(d->tail[1]);
+}
+
+static void lfs1_entry_fromle32(struct lfs1_disk_entry *d) {
+    d->u.dir[0] = lfs_fromle32(d->u.dir[0]);
+    d->u.dir[1] = lfs_fromle32(d->u.dir[1]);
+}
+
+static void lfs1_entry_tole32(struct lfs1_disk_entry *d) {
+    d->u.dir[0] = lfs_tole32(d->u.dir[0]);
+    d->u.dir[1] = lfs_tole32(d->u.dir[1]);
+}
+
+static void lfs1_superblock_fromle32(struct lfs1_disk_superblock *d) {
+    d->root[0]     = lfs_fromle32(d->root[0]);
+    d->root[1]     = lfs_fromle32(d->root[1]);
+    d->block_size  = lfs_fromle32(d->block_size);
+    d->block_count = lfs_fromle32(d->block_count);
+    d->version     = lfs_fromle32(d->version);
+}
+
+
+///// Metadata pair and directory operations ///
+static inline lfs_size_t lfs1_entry_size(const lfs1_entry_t *entry) {
+    return 4 + entry->d.elen + entry->d.alen + entry->d.nlen;
+}
+
+static int lfs1_dir_fetch(lfs_t *lfs,
+        lfs1_dir_t *dir, const lfs_block_t pair[2]) {
+    // copy out pair, otherwise may be aliasing dir
+    const lfs_block_t tpair[2] = {pair[0], pair[1]};
+    bool valid = false;
+
+    // check both blocks for the most recent revision
+    for (int i = 0; i < 2; i++) {
+        struct lfs1_disk_dir test;
+        int err = lfs1_bd_read(lfs, tpair[i], 0, &test, sizeof(test));
+        lfs1_dir_fromle32(&test);
+        if (err) {
+            if (err == LFS_ERR_CORRUPT) {
+                continue;
+            }
+            return err;
+        }
+
+        if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) {
+            continue;
+        }
+
+        if ((0x7fffffff & test.size) < sizeof(test)+4 ||
+            (0x7fffffff & test.size) > lfs->cfg->block_size) {
+            continue;
+        }
+
+        uint32_t crc = 0xffffffff;
+        lfs1_dir_tole32(&test);
+        lfs1_crc(&crc, &test, sizeof(test));
+        lfs1_dir_fromle32(&test);
+        err = lfs1_bd_crc(lfs, tpair[i], sizeof(test),
+                (0x7fffffff & test.size) - sizeof(test), &crc);
+        if (err) {
+            if (err == LFS_ERR_CORRUPT) {
+                continue;
+            }
+            return err;
+        }
+
+        if (crc != 0) {
+            continue;
+        }
+
+        valid = true;
+
+        // setup dir in case it's valid
+        dir->pair[0] = tpair[(i+0) % 2];
+        dir->pair[1] = tpair[(i+1) % 2];
+        dir->off = sizeof(dir->d);
+        dir->d = test;
+    }
+
+    if (!valid) {
+        LFS_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}",
+                tpair[0], tpair[1]);
+        return LFS_ERR_CORRUPT;
+    }
+
+    return 0;
+}
+
+static int lfs1_dir_next(lfs_t *lfs, lfs1_dir_t *dir, lfs1_entry_t *entry) {
+    while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)-4) {
+        if (!(0x80000000 & dir->d.size)) {
+            entry->off = dir->off;
+            return LFS_ERR_NOENT;
+        }
+
+        int err = lfs1_dir_fetch(lfs, dir, dir->d.tail);
+        if (err) {
+            return err;
+        }
+
+        dir->off = sizeof(dir->d);
+        dir->pos += sizeof(dir->d) + 4;
+    }
+
+    int err = lfs1_bd_read(lfs, dir->pair[0], dir->off,
+            &entry->d, sizeof(entry->d));
+    lfs1_entry_fromle32(&entry->d);
+    if (err) {
+        return err;
+    }
+
+    entry->off = dir->off;
+    dir->off += lfs1_entry_size(entry);
+    dir->pos += lfs1_entry_size(entry);
+    return 0;
+}
+
+/// littlefs v1 specific operations ///
+int lfs1_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
+    if (lfs_pair_isnull(lfs->lfs1->root)) {
+        return 0;
+    }
+
+    // iterate over metadata pairs
+    lfs1_dir_t dir;
+    lfs1_entry_t entry;
+    lfs_block_t cwd[2] = {0, 1};
+
+    while (true) {
+        for (int i = 0; i < 2; i++) {
+            int err = cb(data, cwd[i]);
+            if (err) {
+                return err;
+            }
+        }
+
+        int err = lfs1_dir_fetch(lfs, &dir, cwd);
+        if (err) {
+            return err;
+        }
+
+        // iterate over contents
+        while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size)-4) {
+            err = lfs1_bd_read(lfs, dir.pair[0], dir.off,
+                    &entry.d, sizeof(entry.d));
+            lfs1_entry_fromle32(&entry.d);
+            if (err) {
+                return err;
+            }
+
+            dir.off += lfs1_entry_size(&entry);
+            if ((0x70 & entry.d.type) == (0x70 & LFS1_TYPE_REG)) {
+                err = lfs_ctz_traverse(lfs, NULL, &lfs->rcache,
+                        entry.d.u.file.head, entry.d.u.file.size, cb, data);
+                if (err) {
+                    return err;
+                }
+            }
+        }
+
+        // we also need to check if we contain a threaded v2 directory
+        lfs_mdir_t dir2 = {.split=true, .tail={cwd[0], cwd[1]}};
+        while (dir2.split) {
+            err = lfs_dir_fetch(lfs, &dir2, dir2.tail);
+            if (err) {
+                break;
+            }
+
+            for (int i = 0; i < 2; i++) {
+                err = cb(data, dir2.pair[i]);
+                if (err) {
+                    return err;
+                }
+            }
+        }
+
+        cwd[0] = dir.d.tail[0];
+        cwd[1] = dir.d.tail[1];
+
+        if (lfs_pair_isnull(cwd)) {
+            break;
+        }
+    }
+
+    return 0;
+}
+
+static int lfs1_moved(lfs_t *lfs, const void *e) {
+    if (lfs_pair_isnull(lfs->lfs1->root)) {
+        return 0;
+    }
+
+    // skip superblock
+    lfs1_dir_t cwd;
+    int err = lfs1_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1});
+    if (err) {
+        return err;
+    }
+
+    // iterate over all directory directory entries
+    lfs1_entry_t entry;
+    while (!lfs_pair_isnull(cwd.d.tail)) {
+        err = lfs1_dir_fetch(lfs, &cwd, cwd.d.tail);
+        if (err) {
+            return err;
+        }
+
+        while (true) {
+            err = lfs1_dir_next(lfs, &cwd, &entry);
+            if (err && err != LFS_ERR_NOENT) {
+                return err;
+            }
+
+            if (err == LFS_ERR_NOENT) {
+                break;
+            }
+
+            if (!(0x80 & entry.d.type) &&
+                 memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+/// Filesystem operations ///
+static int lfs1_mount(lfs_t *lfs, struct lfs1 *lfs1,
+        const struct lfs_config *cfg) {
+    int err = 0;
+    {
+        err = lfs_init(lfs, cfg);
+        if (err) {
+            return err;
+        }
+
+        lfs->lfs1 = lfs1;
+        lfs->lfs1->root[0] = LFS_BLOCK_NULL;
+        lfs->lfs1->root[1] = LFS_BLOCK_NULL;
+
+        // setup free lookahead
+        lfs->free.off = 0;
+        lfs->free.size = 0;
+        lfs->free.i = 0;
+        lfs_alloc_ack(lfs);
+
+        // load superblock
+        lfs1_dir_t dir;
+        lfs1_superblock_t superblock;
+        err = lfs1_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1});
+        if (err && err != LFS_ERR_CORRUPT) {
+            goto cleanup;
+        }
+
+        if (!err) {
+            err = lfs1_bd_read(lfs, dir.pair[0], sizeof(dir.d),
+                    &superblock.d, sizeof(superblock.d));
+            lfs1_superblock_fromle32(&superblock.d);
+            if (err) {
+                goto cleanup;
+            }
+
+            lfs->lfs1->root[0] = superblock.d.root[0];
+            lfs->lfs1->root[1] = superblock.d.root[1];
+        }
+
+        if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) {
+            LFS_ERROR("Invalid superblock at {0x%"PRIx32", 0x%"PRIx32"}",
+                    0, 1);
+            err = LFS_ERR_CORRUPT;
+            goto cleanup;
+        }
+
+        uint16_t major_version = (0xffff & (superblock.d.version >> 16));
+        uint16_t minor_version = (0xffff & (superblock.d.version >>  0));
+        if ((major_version != LFS1_DISK_VERSION_MAJOR ||
+             minor_version > LFS1_DISK_VERSION_MINOR)) {
+            LFS_ERROR("Invalid version v%d.%d", major_version, minor_version);
+            err = LFS_ERR_INVAL;
+            goto cleanup;
+        }
+
+        return 0;
+    }
+
+cleanup:
+    lfs_deinit(lfs);
+    return err;
+}
+
+static int lfs1_unmount(lfs_t *lfs) {
+    return lfs_deinit(lfs);
+}
+
+/// v1 migration ///
+int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg) {
+    LFS_TRACE("lfs_migrate(%p, %p {.context=%p, "
+                ".read=%p, .prog=%p, .erase=%p, .sync=%p, "
+                ".read_size=%"PRIu32", .prog_size=%"PRIu32", "
+                ".block_size=%"PRIu32", .block_count=%"PRIu32", "
+                ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", "
+                ".lookahead_size=%"PRIu32", .read_buffer=%p, "
+                ".prog_buffer=%p, .lookahead_buffer=%p, "
+                ".name_max=%"PRIu32", .file_max=%"PRIu32", "
+                ".attr_max=%"PRIu32"})",
+            (void*)lfs, (void*)cfg, cfg->context,
+            (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog,
+            (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync,
+            cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count,
+            cfg->block_cycles, cfg->cache_size, cfg->lookahead_size,
+            cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer,
+            cfg->name_max, cfg->file_max, cfg->attr_max);
+    struct lfs1 lfs1;
+    int err = lfs1_mount(lfs, &lfs1, cfg);
+    if (err) {
+        LFS_TRACE("lfs_migrate -> %d", err);
+        return err;
+    }
+
+    {
+        // iterate through each directory, copying over entries
+        // into new directory
+        lfs1_dir_t dir1;
+        lfs_mdir_t dir2;
+        dir1.d.tail[0] = lfs->lfs1->root[0];
+        dir1.d.tail[1] = lfs->lfs1->root[1];
+        while (!lfs_pair_isnull(dir1.d.tail)) {
+            // iterate old dir
+            err = lfs1_dir_fetch(lfs, &dir1, dir1.d.tail);
+            if (err) {
+                goto cleanup;
+            }
+
+            // create new dir and bind as temporary pretend root
+            err = lfs_dir_alloc(lfs, &dir2);
+            if (err) {
+                goto cleanup;
+            }
+
+            dir2.rev = dir1.d.rev;
+            dir1.head[0] = dir1.pair[0];
+            dir1.head[1] = dir1.pair[1];
+            lfs->root[0] = dir2.pair[0];
+            lfs->root[1] = dir2.pair[1];
+
+            err = lfs_dir_commit(lfs, &dir2, NULL, 0);
+            if (err) {
+                goto cleanup;
+            }
+
+            while (true) {
+                lfs1_entry_t entry1;
+                err = lfs1_dir_next(lfs, &dir1, &entry1);
+                if (err && err != LFS_ERR_NOENT) {
+                    goto cleanup;
+                }
+
+                if (err == LFS_ERR_NOENT) {
+                    break;
+                }
+
+                // check that entry has not been moved
+                if (entry1.d.type & 0x80) {
+                    int moved = lfs1_moved(lfs, &entry1.d.u);
+                    if (moved < 0) {
+                        err = moved;
+                        goto cleanup;
+                    }
+
+                    if (moved) {
+                        continue;
+                    }
+
+                    entry1.d.type &= ~0x80;
+                }
+
+                // also fetch name
+                char name[LFS_NAME_MAX+1];
+                memset(name, 0, sizeof(name));
+                err = lfs1_bd_read(lfs, dir1.pair[0],
+                        entry1.off + 4+entry1.d.elen+entry1.d.alen,
+                        name, entry1.d.nlen);
+                if (err) {
+                    goto cleanup;
+                }
+
+                bool isdir = (entry1.d.type == LFS1_TYPE_DIR);
+
+                // create entry in new dir
+                err = lfs_dir_fetch(lfs, &dir2, lfs->root);
+                if (err) {
+                    goto cleanup;
+                }
+
+                uint16_t id;
+                err = lfs_dir_find(lfs, &dir2, &(const char*){name}, &id);
+                if (!(err == LFS_ERR_NOENT && id != 0x3ff)) {
+                    err = (err < 0) ? err : LFS_ERR_EXIST;
+                    goto cleanup;
+                }
+
+                lfs1_entry_tole32(&entry1.d);
+                err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS(
+                        {LFS_MKTAG(LFS_TYPE_CREATE, id, 0)},
+                        {LFS_MKTAG_IF_ELSE(isdir,
+                            LFS_TYPE_DIR, id, entry1.d.nlen,
+                            LFS_TYPE_REG, id, entry1.d.nlen),
+                                name},
+                        {LFS_MKTAG_IF_ELSE(isdir,
+                            LFS_TYPE_DIRSTRUCT, id, sizeof(entry1.d.u),
+                            LFS_TYPE_CTZSTRUCT, id, sizeof(entry1.d.u)),
+                                &entry1.d.u}));
+                lfs1_entry_fromle32(&entry1.d);
+                if (err) {
+                    goto cleanup;
+                }
+            }
+
+            if (!lfs_pair_isnull(dir1.d.tail)) {
+                // find last block and update tail to thread into fs
+                err = lfs_dir_fetch(lfs, &dir2, lfs->root);
+                if (err) {
+                    goto cleanup;
+                }
+
+                while (dir2.split) {
+                    err = lfs_dir_fetch(lfs, &dir2, dir2.tail);
+                    if (err) {
+                        goto cleanup;
+                    }
+                }
+
+                lfs_pair_tole32(dir2.pair);
+                err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS(
+                        {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir1.d.tail}));
+                lfs_pair_fromle32(dir2.pair);
+                if (err) {
+                    goto cleanup;
+                }
+            }
+
+            // Copy over first block to thread into fs. Unfortunately
+            // if this fails there is not much we can do.
+            LFS_DEBUG("Migrating {0x%"PRIx32", 0x%"PRIx32"} "
+                        "-> {0x%"PRIx32", 0x%"PRIx32"}",
+                    lfs->root[0], lfs->root[1], dir1.head[0], dir1.head[1]);
+
+            err = lfs_bd_erase(lfs, dir1.head[1]);
+            if (err) {
+                goto cleanup;
+            }
+
+            err = lfs_dir_fetch(lfs, &dir2, lfs->root);
+            if (err) {
+                goto cleanup;
+            }
+
+            for (lfs_off_t i = 0; i < dir2.off; i++) {
+                uint8_t dat;
+                err = lfs_bd_read(lfs,
+                        NULL, &lfs->rcache, dir2.off,
+                        dir2.pair[0], i, &dat, 1);
+                if (err) {
+                    goto cleanup;
+                }
+
+                err = lfs_bd_prog(lfs,
+                        &lfs->pcache, &lfs->rcache, true,
+                        dir1.head[1], i, &dat, 1);
+                if (err) {
+                    goto cleanup;
+                }
+            }
+
+            err = lfs_bd_flush(lfs, &lfs->pcache, &lfs->rcache, true);
+            if (err) {
+                goto cleanup;
+            }
+        }
+
+        // Create new superblock. This marks a successful migration!
+        err = lfs1_dir_fetch(lfs, &dir1, (const lfs_block_t[2]){0, 1});
+        if (err) {
+            goto cleanup;
+        }
+
+        dir2.pair[0] = dir1.pair[0];
+        dir2.pair[1] = dir1.pair[1];
+        dir2.rev = dir1.d.rev;
+        dir2.off = sizeof(dir2.rev);
+        dir2.etag = 0xffffffff;
+        dir2.count = 0;
+        dir2.tail[0] = lfs->lfs1->root[0];
+        dir2.tail[1] = lfs->lfs1->root[1];
+        dir2.erased = false;
+        dir2.split = true;
+
+        lfs_superblock_t superblock = {
+            .version     = LFS_DISK_VERSION,
+            .block_size  = lfs->cfg->block_size,
+            .block_count = lfs->cfg->block_count,
+            .name_max    = lfs->name_max,
+            .file_max    = lfs->file_max,
+            .attr_max    = lfs->attr_max,
+        };
+
+        lfs_superblock_tole32(&superblock);
+        err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_CREATE, 0, 0)},
+                {LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"},
+                {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
+                    &superblock}));
+        if (err) {
+            goto cleanup;
+        }
+
+        // sanity check that fetch works
+        err = lfs_dir_fetch(lfs, &dir2, (const lfs_block_t[2]){0, 1});
+        if (err) {
+            goto cleanup;
+        }
+
+        // force compaction to prevent accidentally mounting v1
+        dir2.erased = false;
+        err = lfs_dir_commit(lfs, &dir2, NULL, 0);
+        if (err) {
+            goto cleanup;
+        }
+    }
+
+cleanup:
+    lfs1_unmount(lfs);
+    LFS_TRACE("lfs_migrate -> %d", err);
+    return err;
+}
+
+#endif

+ 655 - 0
src/lfs.h

@@ -0,0 +1,655 @@
+/*
+ * The little filesystem
+ *
+ * Copyright (c) 2017, Arm Limited. All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#ifndef LFS_H
+#define LFS_H
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+
+/// Version info ///
+
+// Software library version
+// Major (top-nibble), incremented on backwards incompatible changes
+// Minor (bottom-nibble), incremented on feature additions
+#define LFS_VERSION 0x00020002
+#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 0x00020000
+#define LFS_DISK_VERSION_MAJOR (0xffff & (LFS_DISK_VERSION >> 16))
+#define LFS_DISK_VERSION_MINOR (0xffff & (LFS_DISK_VERSION >>  0))
+
+
+/// Definitions ///
+
+// Type definitions
+typedef uint32_t lfs_size_t;
+typedef uint32_t lfs_off_t;
+
+typedef int32_t  lfs_ssize_t;
+typedef int32_t  lfs_soff_t;
+
+typedef uint32_t lfs_block_t;
+
+// 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
+
+// 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     = -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 {
+    // 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
+
+    // internally used flags
+    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
+    LFS_F_OPENED  = 0x200000, // File has been opened
+};
+
+// File seek flags
+enum lfs_whence_flags {
+    LFS_SEEK_SET = 0,   // Seek relative to an absolute position
+    LFS_SEEK_CUR = 1,   // Seek relative to the current file position
+    LFS_SEEK_END = 2,   // Seek relative to the end of the file
+};
+
+
+// Configuration provided during initialization of the littlefs
+struct lfs_config {
+    // Opaque user provided context that can be used to pass
+    // information to the block device operations
+    void *context;
+
+    // Read a region in a block. Negative error codes are propogated
+    // to the user.
+    int (*read)(const struct lfs_config *c, lfs_block_t block,
+            lfs_off_t off, void *buffer, lfs_size_t size);
+
+    // Program a region in a block. The block must have previously
+    // been erased. Negative error codes are propogated to the user.
+    // May return LFS_ERR_CORRUPT if the block should be considered bad.
+    int (*prog)(const struct lfs_config *c, lfs_block_t block,
+            lfs_off_t off, const void *buffer, lfs_size_t size);
+
+    // Erase a block. A block must be erased before being programmed.
+    // The state of an erased block is undefined. Negative error codes
+    // are propogated to the user.
+    // May return LFS_ERR_CORRUPT if the block should be considered bad.
+    int (*erase)(const struct lfs_config *c, lfs_block_t block);
+
+    // Sync the state of the underlying block device. Negative error codes
+    // are propogated to the user.
+    int (*sync)(const struct lfs_config *c);
+
+    // 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. 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, 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 erase cycles before littlefs evicts metadata logs and moves 
+    // the metadata to another block. Suggested values are in the
+    // range 100-1000, with large values having better performance at the cost
+    // of less consistent wear distribution.
+    //
+    // Set to -1 to disable block-level wear-leveling.
+    int32_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 8.
+    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 cache_size.
+    // By default lfs_malloc is used to allocate this buffer.
+    void *prog_buffer;
+
+    // Optional statically allocated lookahead buffer. Must be lookahead_size
+    // and aligned to a 32-bit boundary. By default lfs_malloc is used to
+    // allocate this buffer.
+    void *lookahead_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
+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. Limited to 32-bits.
+    lfs_size_t size;
+
+    // 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;
+
+    // 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;
+};
+
+
+/// 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;
+    uint16_t id;
+    uint8_t type;
+    lfs_mdir_t m;
+
+    struct lfs_ctz {
+        lfs_block_t head;
+        lfs_size_t size;
+    } ctz;
+
+    uint32_t flags;
+    lfs_off_t pos;
+    lfs_block_t block;
+    lfs_off_t off;
+    lfs_cache_t cache;
+
+    const struct lfs_file_config *cfg;
+} lfs_file_t;
+
+typedef struct lfs_superblock {
+    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_gstate {
+    uint32_t tag;
+    lfs_block_t pair[2];
+} lfs_gstate_t;
+
+// The littlefs filesystem type
+typedef struct lfs {
+    lfs_cache_t rcache;
+    lfs_cache_t pcache;
+
+    lfs_block_t root[2];
+    struct lfs_mlist {
+        struct lfs_mlist *next;
+        uint16_t id;
+        uint8_t type;
+        lfs_mdir_t m;
+    } *mlist;
+    uint32_t seed;
+
+    lfs_gstate_t gstate;
+    lfs_gstate_t gdisk;
+    lfs_gstate_t gdelta;
+
+    struct lfs_free {
+        lfs_block_t off;
+        lfs_block_t size;
+        lfs_block_t i;
+        lfs_block_t ack;
+        uint32_t *buffer;
+    } free;
+
+    const struct lfs_config *cfg;
+    lfs_size_t name_max;
+    lfs_size_t file_max;
+    lfs_size_t attr_max;
+
+#ifdef LFS_MIGRATE
+    struct lfs1 *lfs1;
+#endif
+} lfs_t;
+
+
+/// Filesystem functions ///
+
+// Format a block device with the 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_format(lfs_t *lfs, const struct lfs_config *config);
+
+// Mounts a littlefs
+//
+// Requires a littlefs object and config struct. Multiple filesystems
+// may be mounted simultaneously with multiple littlefs objects. Both
+// lfs and config must be allocated while mounted. The config struct must
+// be zeroed for defaults and backwards compatibility.
+//
+// Returns a negative error code on failure.
+int lfs_mount(lfs_t *lfs, const struct lfs_config *config);
+
+// Unmounts a littlefs
+//
+// Does nothing besides releasing any allocated resources.
+// Returns a negative error code on failure.
+int lfs_unmount(lfs_t *lfs);
+
+/// General operations ///
+
+// Removes a file or directory
+//
+// If removing a directory, the directory must be empty.
+// Returns a negative error code on failure.
+int lfs_remove(lfs_t *lfs, const char *path);
+
+// Rename or move a file or directory
+//
+// If the destination exists, it must match the source in type.
+// If the destination is a directory, the directory must be empty.
+//
+// Returns a negative error code on failure.
+int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath);
+
+// Find info about a file or directory
+//
+// Fills out the info structure, based on the specified file or directory.
+// 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 ///
+
+// Open a file
+//
+// The mode that the file is opened in is determined by the flags, which
+// are values from the enum lfs_open_flags that are bitwise-ored together.
+//
+// Returns a negative error code on failure.
+int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
+        const char *path, int flags);
+
+// Open a file with extra configuration
+//
+// The mode that the file is opened in is determined by the flags, which
+// are values from the enum lfs_open_flags that are bitwise-ored together.
+//
+// The config struct provides additional config options per file as described
+// above. The config struct must be allocated while the file is open, and the
+// config struct must be zeroed for defaults and backwards compatibility.
+//
+// Returns a negative error code on failure.
+int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file,
+        const char *path, int flags,
+        const struct lfs_file_config *config);
+
+// Close a file
+//
+// Any pending writes are written out to storage as though
+// sync had been called and releases any allocated resources.
+//
+// Returns a negative error code on failure.
+int lfs_file_close(lfs_t *lfs, lfs_file_t *file);
+
+// Synchronize a file on storage
+//
+// Any pending writes are written out to storage.
+// Returns a negative error code on failure.
+int lfs_file_sync(lfs_t *lfs, lfs_file_t *file);
+
+// Read data from file
+//
+// Takes a buffer and size indicating where to store the read data.
+// Returns the number of bytes read, or a negative error code on failure.
+lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file,
+        void *buffer, lfs_size_t size);
+
+// Write data to file
+//
+// Takes a buffer and size indicating the data to write. The file will not
+// actually be updated on the storage until either sync or close is called.
+//
+// Returns the number of bytes written, or a negative error code on failure.
+lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
+        const void *buffer, lfs_size_t size);
+
+// Change the position of the file
+//
+// The change in position is determined by the offset and whence flag.
+// Returns the new position of the file, or a negative error code on failure.
+lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file,
+        lfs_soff_t off, int whence);
+
+// Truncates the size of the file to the specified size
+//
+// Returns a negative error code on failure.
+int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size);
+
+// Return the position of the file
+//
+// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR)
+// Returns the position of the file, or a negative error code on failure.
+lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file);
+
+// Change the position of the file to the beginning of the file
+//
+// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_SET)
+// Returns a negative error code on failure.
+int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file);
+
+// Return the size of the file
+//
+// Similar to lfs_file_seek(lfs, file, 0, LFS_SEEK_END)
+// Returns the size of the file, or a negative error code on failure.
+lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file);
+
+
+/// Directory operations ///
+
+// Create a directory
+//
+// Returns a negative error code on failure.
+int lfs_mkdir(lfs_t *lfs, const char *path);
+
+// Open a directory
+//
+// Once open a directory can be used with read to iterate over files.
+// Returns a negative error code on failure.
+int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path);
+
+// Close a directory
+//
+// Releases any allocated resources.
+// Returns a negative error code on failure.
+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 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
+//
+// The new off must be a value previous returned from tell and specifies
+// an absolute offset in the directory seek.
+//
+// Returns a negative error code on failure.
+int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off);
+
+// Return the position of the directory
+//
+// The returned offset is only meant to be consumed by seek and may not make
+// sense, but does indicate the current position in the directory iteration.
+//
+// Returns the position of the directory, or a negative error code on failure.
+lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir);
+
+// Change the position of the directory to the beginning of the directory
+//
+// Returns a negative error code on failure.
+int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir);
+
+
+/// 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
+//
+// The provided callback will be called with each block address that is
+// currently in use by the filesystem. This can be used to determine which
+// blocks are in use or how much of the storage is available.
+//
+// Returns a negative error code on failure.
+int lfs_fs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data);
+
+#ifdef LFS_MIGRATE
+// Attempts to migrate a previous version of littlefs
+//
+// 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_migrate(lfs_t *lfs, const struct lfs_config *cfg);
+#endif
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif

+ 33 - 0
src/lfs_util.c

@@ -0,0 +1,33 @@
+/*
+ * lfs util functions
+ *
+ * Copyright (c) 2017, Arm Limited. All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#include "lfs_util.h"
+
+// Only compile if user does not provide custom config
+#ifndef LFS_CONFIG
+
+
+// Software CRC implementation with small lookup table
+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,
+        0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
+        0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c,
+    };
+
+    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];
+    }
+
+    return crc;
+}
+
+
+#endif

+ 234 - 0
src/lfs_util.h

@@ -0,0 +1,234 @@
+/*
+ * lfs utility functions
+ *
+ * Copyright (c) 2017, Arm Limited. All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#ifndef LFS_UTIL_H
+#define LFS_UTIL_H
+
+// Users can override lfs_util.h with their own configuration by defining
+// 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.
+#ifdef LFS_CONFIG
+#define LFS_STRINGIZE(x) LFS_STRINGIZE2(x)
+#define LFS_STRINGIZE2(x) #x
+#include LFS_STRINGIZE(LFS_CONFIG)
+#else
+
+// System includes
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <inttypes.h>
+
+#ifndef LFS_NO_MALLOC
+#include <stdlib.h>
+#endif
+#ifndef LFS_NO_ASSERT
+#include <assert.h>
+#endif
+#if !defined(LFS_NO_DEBUG) || \
+        !defined(LFS_NO_WARN) || \
+        !defined(LFS_NO_ERROR) || \
+        defined(LFS_YES_TRACE)
+#include <stdio.h>
+#endif
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+
+// Macros, may be replaced by system specific wrappers. Arguments to these
+// macros must not have side-effects as the macros can be removed for a smaller
+// code footprint
+
+// Logging functions
+#ifdef LFS_YES_TRACE
+#define LFS_TRACE_(fmt, ...) \
+    printf("%s:%d:trace: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__)
+#define LFS_TRACE(...) LFS_TRACE_(__VA_ARGS__, "")
+#else
+#define LFS_TRACE(...)
+#endif
+
+#ifndef LFS_NO_DEBUG
+#define LFS_DEBUG_(fmt, ...) \
+    printf("%s:%d:debug: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__)
+#define LFS_DEBUG(...) LFS_DEBUG_(__VA_ARGS__, "")
+#else
+#define LFS_DEBUG(...)
+#endif
+
+#ifndef LFS_NO_WARN
+#define LFS_WARN_(fmt, ...) \
+    printf("%s:%d:warn: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__)
+#define LFS_WARN(...) LFS_WARN_(__VA_ARGS__, "")
+#else
+#define LFS_WARN(...)
+#endif
+
+#ifndef LFS_NO_ERROR
+#define LFS_ERROR_(fmt, ...) \
+    printf("%s:%d:error: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__)
+#define LFS_ERROR(...) LFS_ERROR_(__VA_ARGS__, "")
+#else
+#define LFS_ERROR(...)
+#endif
+
+// Runtime assertions
+#ifndef LFS_NO_ASSERT
+#define LFS_ASSERT(test) assert(test)
+#else
+#define LFS_ASSERT(test)
+#endif
+
+
+// Builtin functions, these may be replaced by more efficient
+// toolchain-specific implementations. LFS_NO_INTRINSICS falls back to a more
+// expensive basic C implementation for debugging purposes
+
+// Min/max functions for unsigned 32-bit numbers
+static inline uint32_t lfs_max(uint32_t a, uint32_t b) {
+    return (a > b) ? a : b;
+}
+
+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 smallest power of 2 greater than or equal to a
+static inline uint32_t lfs_npw2(uint32_t a) {
+#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM))
+    return 32 - __builtin_clz(a-1);
+#else
+    uint32_t r = 0;
+    uint32_t s;
+    a -= 1;
+    s = (a > 0xffff) << 4; a >>= s; r |= s;
+    s = (a > 0xff  ) << 3; a >>= s; r |= s;
+    s = (a > 0xf   ) << 2; a >>= s; r |= s;
+    s = (a > 0x3   ) << 1; a >>= s; r |= s;
+    return (r | (a >> 1)) + 1;
+#endif
+}
+
+// Count the number of trailing binary zeros in a
+// lfs_ctz(0) may be undefined
+static inline uint32_t lfs_ctz(uint32_t a) {
+#if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__)
+    return __builtin_ctz(a);
+#else
+    return lfs_npw2((a & -a) + 1) - 1;
+#endif
+}
+
+// Count the number of binary ones in a
+static inline uint32_t lfs_popc(uint32_t a) {
+#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM))
+    return __builtin_popcount(a);
+#else
+    a = a - ((a >> 1) & 0x55555555);
+    a = (a & 0x33333333) + ((a >> 2) & 0x33333333);
+    return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24;
+#endif
+}
+
+// Find the sequence comparison of a and b, this is the distance
+// between a and b ignoring overflow
+static inline int lfs_scmp(uint32_t a, uint32_t b) {
+    return (int)(unsigned)(a - b);
+}
+
+// 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  ) && defined(  ORDER_LITTLE_ENDIAN  ) &&   BYTE_ORDER   ==   ORDER_LITTLE_ENDIAN  ) || \
+    (defined(__BYTE_ORDER  ) && defined(__ORDER_LITTLE_ENDIAN  ) && __BYTE_ORDER   == __ORDER_LITTLE_ENDIAN  ) || \
+    (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__))
+    return a;
+#elif !defined(LFS_NO_INTRINSICS) && ( \
+    (defined(  BYTE_ORDER  ) && defined(  ORDER_BIG_ENDIAN  ) &&   BYTE_ORDER   ==   ORDER_BIG_ENDIAN  ) || \
+    (defined(__BYTE_ORDER  ) && defined(__ORDER_BIG_ENDIAN  ) && __BYTE_ORDER   == __ORDER_BIG_ENDIAN  ) || \
+    (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__))
+    return __builtin_bswap32(a);
+#else
+    return (((uint8_t*)&a)[0] <<  0) |
+           (((uint8_t*)&a)[1] <<  8) |
+           (((uint8_t*)&a)[2] << 16) |
+           (((uint8_t*)&a)[3] << 24);
+#endif
+}
+
+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  ) && defined(  ORDER_LITTLE_ENDIAN  ) &&   BYTE_ORDER   ==   ORDER_LITTLE_ENDIAN  ) || \
+    (defined(__BYTE_ORDER  ) && defined(__ORDER_LITTLE_ENDIAN  ) && __BYTE_ORDER   == __ORDER_LITTLE_ENDIAN  ) || \
+    (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__))
+    return __builtin_bswap32(a);
+#elif !defined(LFS_NO_INTRINSICS) && ( \
+    (defined(  BYTE_ORDER  ) && defined(  ORDER_BIG_ENDIAN  ) &&   BYTE_ORDER   ==   ORDER_BIG_ENDIAN  ) || \
+    (defined(__BYTE_ORDER  ) && defined(__ORDER_BIG_ENDIAN  ) && __BYTE_ORDER   == __ORDER_BIG_ENDIAN  ) || \
+    (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __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
+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);
+#else
+    (void)size;
+    return NULL;
+#endif
+}
+
+// Deallocate memory, only used if buffers are not provided to littlefs
+static inline void lfs_free(void *p) {
+#ifndef LFS_NO_MALLOC
+    free(p);
+#else
+    (void)p;
+#endif
+}
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif
+#endif

+ 58 - 0
src/littlefs_api.c

@@ -0,0 +1,58 @@
+/**
+ * @file littlefs_api.c
+ * @brief Maps the HAL of esp_partition <-> littlefs
+ * @author Brian Pugh
+ */
+
+#define ESP_LOCAL_LOG_LEVEL ESP_LOG_INFO
+
+#include "esp_log.h"
+#include "esp_partition.h"
+#include "esp_vfs.h"
+#include "lfs.h"
+#include "esp_littlefs.h"
+#include "littlefs_api.h"
+
+static const char TAG[] = "esp_littlefs_api";
+
+int littlefs_api_read(const struct lfs_config *c, lfs_block_t block,
+        lfs_off_t off, void *buffer, lfs_size_t size) {
+    esp_littlefs_t * efs = c->context;
+    size_t part_off = (block * c->block_size) + off;
+    esp_err_t err = esp_partition_read(efs->partition, part_off, buffer, size);
+    if (err) {
+        ESP_LOGE(TAG, "failed to read addr %08x, size %08x, err %d", part_off, size, err);
+        return LFS_ERR_IO;
+    }
+    return 0;
+}
+
+int littlefs_api_prog(const struct lfs_config *c, lfs_block_t block,
+        lfs_off_t off, const void *buffer, lfs_size_t size) {
+    esp_littlefs_t * efs = c->context;
+    size_t part_off = (block * c->block_size) + off;
+    esp_err_t err = esp_partition_write(efs->partition, part_off, buffer, size);
+    if (err) {
+        ESP_LOGE(TAG, "failed to write addr %08x, size %08x, err %d", part_off, size, err);
+        return LFS_ERR_IO;
+    }
+    return 0;
+}
+
+int littlefs_api_erase(const struct lfs_config *c, lfs_block_t block) {
+    esp_littlefs_t * efs = c->context;
+    size_t part_off = block * c->block_size;
+    esp_err_t err = esp_partition_erase_range(efs->partition, part_off, c->block_size);
+    if (err) {
+        ESP_LOGE(TAG, "failed to erase addr %08x, size %08x, err %d", part_off, c->block_size, err);
+        return LFS_ERR_IO;
+    }
+    return 0;
+
+}
+
+int littlefs_api_sync(const struct lfs_config *c) {
+    /* Unnecessary for esp-idf */
+    return 0;
+}
+

+ 106 - 0
src/littlefs_api.h

@@ -0,0 +1,106 @@
+#ifndef ESP_LITTLEFS_API_H__
+#define ESP_LITTLEFS_API_H__
+
+#include <stdint.h>
+#include <stddef.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_vfs.h"
+#include "esp_partition.h"
+#include "lfs.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief a file descriptor
+ * That's also a singly linked list used for keeping tracks of all opened file descriptor 
+ *
+ * Shortcomings/potential issues of 32-bit hash (when CONFIG_LITTLEFS_USE_ONLY_HASH) listed here:
+ *     * unlink - If a different file is open that generates a hash collision, it will report an
+ *                error that it cannot unlink an open file.
+ *     * rename - If a different file is open that generates a hash collision with
+ *                src or dst, it will report an error that it cannot rename an open file.
+ * Potential consequences:
+ *    1. A file cannot be deleted while a collision-geneating file is open.
+ *       Worst-case, if the other file is always open during the lifecycle
+ *       of your app, it's collision file cannot be deleted, which in the 
+ *       worst-case could cause storage-capacity issues.
+ *    2. Same as (1), but for renames
+ */
+typedef struct _vfs_littlefs_file_t {
+    lfs_file_t file;
+    uint32_t   hash;
+    struct _vfs_littlefs_file_t * next;       /*!< Pointer to next file in Singly Linked List */
+#ifndef CONFIG_LITTLEFS_USE_ONLY_HASH
+    char     * path;
+#endif
+} vfs_littlefs_file_t;
+
+/**
+ * @brief littlefs definition structure
+ */
+typedef struct {
+    lfs_t *fs;                                /*!< Handle to the underlying littlefs */
+    SemaphoreHandle_t lock;                   /*!< FS lock */
+    const esp_partition_t* partition;         /*!< The partition on which littlefs is located */
+    char base_path[ESP_VFS_PATH_MAX+1];       /*!< Mount point */
+
+    struct lfs_config cfg;                    /*!< littlefs Mount configuration */
+
+    vfs_littlefs_file_t *file;                /*!< Singly Linked List of files */
+
+    vfs_littlefs_file_t **cache;              /*!< A cache of pointers to the opened files */
+    uint16_t             cache_size;          /*!< The cache allocated size (in pointers) */
+    uint16_t             fd_count;            /*!< The count of opened file descriptor used to speed up computation */
+} esp_littlefs_t;
+
+/**
+ * @brief Read a region in a block.
+ *
+ * Negative error codes are propogated to the user.
+ *
+ * @return errorcode. 0 on success.
+ */
+int littlefs_api_read(const struct lfs_config *c, lfs_block_t block,
+        lfs_off_t off, void *buffer, lfs_size_t size);
+
+/**
+ * @brief Program a region in a block.
+ *
+ * The block must have previously been erased. 
+ * Negative error codes are propogated to the user.
+ * May return LFS_ERR_CORRUPT if the block should be considered bad.
+ *
+ * @return errorcode. 0 on success.
+ */
+int littlefs_api_prog(const struct lfs_config *c, lfs_block_t block,
+        lfs_off_t off, const void *buffer, lfs_size_t size);
+
+/**
+ * @brief Erase a block.
+ *
+ * A block must be erased before being programmed.
+ * The state of an erased block is undefined.
+ * Negative error codes are propogated to the user.
+ * May return LFS_ERR_CORRUPT if the block should be considered bad.
+ * @return errorcode. 0 on success.
+ */
+int littlefs_api_erase(const struct lfs_config *c, lfs_block_t block);
+
+/**
+ * @brief Sync the state of the underlying block device.
+ *
+ * Negative error codes are propogated to the user.
+ *
+ * @return errorcode. 0 on success.
+ */
+int littlefs_api_sync(const struct lfs_config *c);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif