diff --git a/.astyleignore b/.astyleignore index 2eb85dacddc..a785025d756 100644 --- a/.astyleignore +++ b/.astyleignore @@ -20,6 +20,7 @@ ^features/netsocket/ppp/source ^features/storage/filesystem/fat/ChaN ^features/storage/filesystem/littlefs/littlefs/ +^features/storage/filesystem/littlefsv2/littlefs/ ^features/unsupported/ ^hal/storage_abstraction ^platform/cxxsupport diff --git a/.travis.yml b/.travis.yml index 56bcc62d509..0c63c681221 100644 --- a/.travis.yml +++ b/.travis.yml @@ -121,7 +121,7 @@ matrix: script: - >- git diff --name-only --diff-filter=d FETCH_HEAD..HEAD \ - | ( grep '.\(c\|cpp\|h\|hpp\)$' || true ) \ + | ( grep '.*\.\(c\|cpp\|h\|hpp\)$' || true ) \ | ( grep -v -f .astyleignore || true ) \ | while read file; do astyle -n --options=.astylerc "${file}"; done - git diff --exit-code --diff-filter=d --color diff --git a/TESTS/configs/baremetal.json b/TESTS/configs/baremetal.json index c6a83768d5c..64e1cbc70bc 100644 --- a/TESTS/configs/baremetal.json +++ b/TESTS/configs/baremetal.json @@ -9,6 +9,7 @@ "psa-compliance-framework", "filesystem", "littlefs", + "littlefs2", "mbed-trace", "device_key", "storage_tdb_internal", diff --git a/features/storage/filesystem/littlefsv2/.mbedignore b/features/storage/filesystem/littlefsv2/.mbedignore new file mode 100644 index 00000000000..05bb3c3e7f7 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/.mbedignore @@ -0,0 +1,3 @@ +littlefs/bd/ +littlefs/tests/ +TESTS/util diff --git a/features/storage/filesystem/littlefsv2/.travis.yml b/features/storage/filesystem/littlefsv2/.travis.yml new file mode 100644 index 00000000000..4cbd93c4b3a --- /dev/null +++ b/features/storage/filesystem/littlefsv2/.travis.yml @@ -0,0 +1,40 @@ +language: python +python: 2.7 + +script: + # Check that example compiles + - sed -n '/``` c++/,/```/{/```/d; p;}' README.md > main.cpp + - PYTHONPATH=mbed-os python mbed-os/tools/make.py -t GCC_ARM -m K82F + --source=. --build=BUILD/K82F/GCC_ARM -j0 + + # Check that tests compile + - rm -rf main.cpp BUILD + - PYTHONPATH=mbed-os python mbed-os/tools/test.py -t GCC_ARM -m K82F + --source=. --build=BUILD/TESTS/K82F/GCC_ARM -j0 + -n 'tests*' + + # Run littlefs functional tests + - make -Clittlefs test QUIET=1 + + # Run littlefs functional tests with different configurations + # Note: r/w size of 64 is default in mbed + - make -Clittlefs test QUIET=1 CFLAGS+="-DLFS2_READ_SIZE=64 -DLFS2_CACHE_SIZE=64" + - make -Clittlefs test QUIET=1 CFLAGS+="-DLFS2_READ_SIZE=1 -DLFS2_CACHE_SIZE=1" + - make -Clittlefs test QUIET=1 CFLAGS+="-DLFS2_READ_SIZE=512 -DLFS2_CACHE_SIZE=512 -DLFS2_BLOCK_CYCLES=16" + - make -Clittlefs test QUIET=1 CFLAGS+="-DLFS2_READ_SIZE=8 -DLFS2_CACHE_SIZE=16 -DLFS2_BLOCK_CYCLES=2" + - make -Clittlefs test QUIET=1 CFLAGS+="-DLFS2_BLOCK_COUNT=1023 -DLFS2_LOOKAHEAD_SIZE=256" + +install: + # Get arm-none-eabi-gcc + - sudo add-apt-repository -y ppa:team-gcc-arm-embedded/ppa + - sudo apt-get update -qq + - sudo apt-get install -qq gcc-arm-embedded + # Get dependencies + - git clone https://github.com/armmbed/mbed-os.git + # Install python dependencies + - pip install -r mbed-os/requirements.txt + - sudo apt-get install python3 python3-pip + - sudo pip3 install toml + # Check versions + - arm-none-eabi-gcc --version + - gcc --version diff --git a/features/storage/filesystem/littlefsv2/LittleFileSystem2.cpp b/features/storage/filesystem/littlefsv2/LittleFileSystem2.cpp new file mode 100644 index 00000000000..2ce32613e76 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/LittleFileSystem2.cpp @@ -0,0 +1,512 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "filesystem/mbed_filesystem.h" +#include "LittleFileSystem2.h" +#include "errno.h" +#include "lfs2.h" +#include "lfs2_util.h" +#include "MbedCRC.h" + +namespace mbed { + +extern "C" uint32_t lfs2_crc(uint32_t crc, const void *buffer, size_t size) +{ + uint32_t initial_xor = lfs2_rbit(crc); + MbedCRC ct(initial_xor, 0x0, true, true); + + ct.compute((void *)buffer, size, &crc); + return crc; +} + +////// Conversion functions ////// +static int lfs2_toerror(int err) +{ + switch (err) { + case LFS2_ERR_OK: + return 0; + case LFS2_ERR_IO: + return -EIO; + case LFS2_ERR_NOENT: + return -ENOENT; + case LFS2_ERR_EXIST: + return -EEXIST; + case LFS2_ERR_NOTDIR: + return -ENOTDIR; + case LFS2_ERR_ISDIR: + return -EISDIR; + case LFS2_ERR_INVAL: + return -EINVAL; + case LFS2_ERR_NOSPC: + return -ENOSPC; + case LFS2_ERR_NOMEM: + return -ENOMEM; + case LFS2_ERR_CORRUPT: + return -EILSEQ; + default: + return err; + } +} + +static int lfs2_fromflags(int flags) +{ + return ( + (((flags & 3) == O_RDONLY) ? LFS2_O_RDONLY : 0) | + (((flags & 3) == O_WRONLY) ? LFS2_O_WRONLY : 0) | + (((flags & 3) == O_RDWR) ? LFS2_O_RDWR : 0) | + ((flags & O_CREAT) ? LFS2_O_CREAT : 0) | + ((flags & O_EXCL) ? LFS2_O_EXCL : 0) | + ((flags & O_TRUNC) ? LFS2_O_TRUNC : 0) | + ((flags & O_APPEND) ? LFS2_O_APPEND : 0)); +} + +static int lfs2_fromwhence(int whence) +{ + switch (whence) { + case SEEK_SET: + return LFS2_SEEK_SET; + case SEEK_CUR: + return LFS2_SEEK_CUR; + case SEEK_END: + return LFS2_SEEK_END; + default: + return whence; + } +} + +static int lfs2_tomode(int type) +{ + int mode = S_IRWXU | S_IRWXG | S_IRWXO; + switch (type) { + case LFS2_TYPE_DIR: + return mode | S_IFDIR; + case LFS2_TYPE_REG: + return mode | S_IFREG; + default: + return 0; + } +} + +static int lfs2_totype(int type) +{ + switch (type) { + case LFS2_TYPE_DIR: + return DT_DIR; + case LFS2_TYPE_REG: + return DT_REG; + default: + return DT_UNKNOWN; + } +} + + +////// Block device operations ////// +static int lfs2_bd_read(const struct lfs2_config *c, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size) +{ + BlockDevice *bd = (BlockDevice *)c->context; + return bd->read(buffer, (bd_addr_t)block * c->block_size + off, size); +} + +static int lfs2_bd_prog(const struct lfs2_config *c, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_size_t size) +{ + BlockDevice *bd = (BlockDevice *)c->context; + return bd->program(buffer, (bd_addr_t)block * c->block_size + off, size); +} + +static int lfs2_bd_erase(const struct lfs2_config *c, lfs2_block_t block) +{ + BlockDevice *bd = (BlockDevice *)c->context; + return bd->erase((bd_addr_t)block * c->block_size, c->block_size); +} + +static int lfs2_bd_sync(const struct lfs2_config *c) +{ + BlockDevice *bd = (BlockDevice *)c->context; + return bd->sync(); +} + + +////// Generic filesystem operations ////// + +// Filesystem implementation (See LittleFileSystem2.h) +LittleFileSystem2::LittleFileSystem2(const char *name, BlockDevice *bd, + lfs2_size_t block_size, uint32_t block_cycles, + lfs2_size_t cache_size, lfs2_size_t lookahead_size) + : FileSystem(name) +{ + memset(&_config, 0, sizeof(_config)); + _config.block_size = block_size; + _config.block_cycles = block_cycles; + _config.cache_size = cache_size; + _config.lookahead_size = lookahead_size; + if (bd) { + mount(bd); + } +} + +LittleFileSystem2::~LittleFileSystem2() +{ + // nop if unmounted + unmount(); +} + +int LittleFileSystem2::mount(BlockDevice *bd) +{ + _mutex.lock(); + _bd = bd; + int err = _bd->init(); + if (err) { + _bd = NULL; + _mutex.unlock(); + return err; + } + + _config.context = bd; + _config.read = lfs2_bd_read; + _config.prog = lfs2_bd_prog; + _config.erase = lfs2_bd_erase; + _config.sync = lfs2_bd_sync; + _config.read_size = bd->get_read_size(); + _config.prog_size = bd->get_program_size(); + _config.block_size = lfs2_max(_config.block_size, (lfs2_size_t)bd->get_erase_size()); + _config.block_count = bd->size() / _config.block_size; + _config.block_cycles = _config.block_cycles; + _config.cache_size = lfs2_max(_config.cache_size, _config.prog_size); + _config.lookahead_size = lfs2_min(_config.lookahead_size, 8 * ((_config.block_count + 63) / 64)); + + err = lfs2_mount(&_lfs, &_config); + if (err) { + _bd = NULL; + _mutex.unlock(); + return lfs2_toerror(err); + } + + _mutex.unlock(); + return 0; +} + +int LittleFileSystem2::unmount() +{ + _mutex.lock(); + int res = 0; + if (_bd) { + int err = lfs2_unmount(&_lfs); + if (err && !res) { + res = lfs2_toerror(err); + } + + err = _bd->deinit(); + if (err && !res) { + res = err; + } + + _bd = NULL; + } + + _mutex.unlock(); + return res; +} + +int LittleFileSystem2::format(BlockDevice *bd, + lfs2_size_t block_size, uint32_t block_cycles, + lfs2_size_t cache_size, lfs2_size_t lookahead_size) +{ + int err = bd->init(); + if (err) { + return err; + } + + lfs2_t _lfs; + struct lfs2_config _config; + + memset(&_config, 0, sizeof(_config)); + _config.context = bd; + _config.read = lfs2_bd_read; + _config.prog = lfs2_bd_prog; + _config.erase = lfs2_bd_erase; + _config.sync = lfs2_bd_sync; + _config.read_size = bd->get_read_size(); + _config.prog_size = bd->get_program_size(); + _config.block_size = lfs2_max(block_size, (lfs2_size_t)bd->get_erase_size()); + _config.block_count = bd->size() / _config.block_size; + _config.block_cycles = block_cycles; + _config.cache_size = lfs2_max(cache_size, _config.prog_size); + _config.lookahead_size = lfs2_min(lookahead_size, 8 * ((_config.block_count + 63) / 64)); + + err = lfs2_format(&_lfs, &_config); + if (err) { + return lfs2_toerror(err); + } + + err = bd->deinit(); + if (err) { + return err; + } + + return 0; +} + +int LittleFileSystem2::reformat(BlockDevice *bd) +{ + _mutex.lock(); + if (_bd) { + if (!bd) { + bd = _bd; + } + + int err = unmount(); + if (err) { + _mutex.unlock(); + return err; + } + } + + if (!bd) { + _mutex.unlock(); + return -ENODEV; + } + + int err = LittleFileSystem2::format(bd, + _config.block_size, + _config.block_cycles, + _config.cache_size, + _config.lookahead_size); + if (err) { + _mutex.unlock(); + return err; + } + + err = mount(bd); + if (err) { + _mutex.unlock(); + return err; + } + + _mutex.unlock(); + return 0; +} + +int LittleFileSystem2::remove(const char *filename) +{ + _mutex.lock(); + int err = lfs2_remove(&_lfs, filename); + _mutex.unlock(); + return lfs2_toerror(err); +} + +int LittleFileSystem2::rename(const char *oldname, const char *newname) +{ + _mutex.lock(); + int err = lfs2_rename(&_lfs, oldname, newname); + _mutex.unlock(); + return lfs2_toerror(err); +} + +int LittleFileSystem2::mkdir(const char *name, mode_t mode) +{ + _mutex.lock(); + int err = lfs2_mkdir(&_lfs, name); + _mutex.unlock(); + return lfs2_toerror(err); +} + +int LittleFileSystem2::stat(const char *name, struct stat *st) +{ + struct lfs2_info info; + _mutex.lock(); + int err = lfs2_stat(&_lfs, name, &info); + _mutex.unlock(); + st->st_size = info.size; + st->st_mode = lfs2_tomode(info.type); + return lfs2_toerror(err); +} + +int LittleFileSystem2::statvfs(const char *name, struct statvfs *st) +{ + memset(st, 0, sizeof(struct statvfs)); + + lfs2_ssize_t in_use = 0; + _mutex.lock(); + in_use = lfs2_fs_size(&_lfs); + _mutex.unlock(); + if (in_use < 0) { + return in_use; + } + + st->f_bsize = _config.block_size; + st->f_frsize = _config.block_size; + st->f_blocks = _config.block_count; + st->f_bfree = _config.block_count - in_use; + st->f_bavail = _config.block_count - in_use; + st->f_namemax = LFS2_NAME_MAX; + return 0; +} + +////// File operations ////// +int LittleFileSystem2::file_open(fs_file_t *file, const char *path, int flags) +{ + lfs2_file_t *f = new lfs2_file_t; + _mutex.lock(); + int err = lfs2_file_open(&_lfs, f, path, lfs2_fromflags(flags)); + _mutex.unlock(); + if (!err) { + *file = f; + } else { + delete f; + } + return lfs2_toerror(err); +} + +int LittleFileSystem2::file_close(fs_file_t file) +{ + lfs2_file_t *f = (lfs2_file_t *)file; + _mutex.lock(); + int err = lfs2_file_close(&_lfs, f); + _mutex.unlock(); + delete f; + return lfs2_toerror(err); +} + +ssize_t LittleFileSystem2::file_read(fs_file_t file, void *buffer, size_t len) +{ + lfs2_file_t *f = (lfs2_file_t *)file; + _mutex.lock(); + lfs2_ssize_t res = lfs2_file_read(&_lfs, f, buffer, len); + _mutex.unlock(); + return lfs2_toerror(res); +} + +ssize_t LittleFileSystem2::file_write(fs_file_t file, const void *buffer, size_t len) +{ + lfs2_file_t *f = (lfs2_file_t *)file; + _mutex.lock(); + lfs2_ssize_t res = lfs2_file_write(&_lfs, f, buffer, len); + _mutex.unlock(); + return lfs2_toerror(res); +} + +int LittleFileSystem2::file_sync(fs_file_t file) +{ + lfs2_file_t *f = (lfs2_file_t *)file; + _mutex.lock(); + int err = lfs2_file_sync(&_lfs, f); + _mutex.unlock(); + return lfs2_toerror(err); +} + +off_t LittleFileSystem2::file_seek(fs_file_t file, off_t offset, int whence) +{ + lfs2_file_t *f = (lfs2_file_t *)file; + _mutex.lock(); + off_t res = lfs2_file_seek(&_lfs, f, offset, lfs2_fromwhence(whence)); + _mutex.unlock(); + return lfs2_toerror(res); +} + +off_t LittleFileSystem2::file_tell(fs_file_t file) +{ + lfs2_file_t *f = (lfs2_file_t *)file; + _mutex.lock(); + off_t res = lfs2_file_tell(&_lfs, f); + _mutex.unlock(); + return lfs2_toerror(res); +} + +off_t LittleFileSystem2::file_size(fs_file_t file) +{ + lfs2_file_t *f = (lfs2_file_t *)file; + _mutex.lock(); + off_t res = lfs2_file_size(&_lfs, f); + _mutex.unlock(); + return lfs2_toerror(res); +} + +int LittleFileSystem2::file_truncate(fs_file_t file, off_t length) +{ + lfs2_file_t *f = (lfs2_file_t *)file; + _mutex.lock(); + int err = lfs2_file_truncate(&_lfs, f, length); + _mutex.unlock(); + return lfs2_toerror(err); +} + + +////// Dir operations ////// +int LittleFileSystem2::dir_open(fs_dir_t *dir, const char *path) +{ + lfs2_dir_t *d = new lfs2_dir_t; + _mutex.lock(); + int err = lfs2_dir_open(&_lfs, d, path); + _mutex.unlock(); + if (!err) { + *dir = d; + } else { + delete d; + } + return lfs2_toerror(err); +} + +int LittleFileSystem2::dir_close(fs_dir_t dir) +{ + lfs2_dir_t *d = (lfs2_dir_t *)dir; + _mutex.lock(); + int err = lfs2_dir_close(&_lfs, d); + _mutex.unlock(); + delete d; + return lfs2_toerror(err); +} + +ssize_t LittleFileSystem2::dir_read(fs_dir_t dir, struct dirent *ent) +{ + lfs2_dir_t *d = (lfs2_dir_t *)dir; + struct lfs2_info info; + _mutex.lock(); + int res = lfs2_dir_read(&_lfs, d, &info); + _mutex.unlock(); + if (res == 1) { + ent->d_type = lfs2_totype(info.type); + strcpy(ent->d_name, info.name); + } + return lfs2_toerror(res); +} + +void LittleFileSystem2::dir_seek(fs_dir_t dir, off_t offset) +{ + lfs2_dir_t *d = (lfs2_dir_t *)dir; + _mutex.lock(); + lfs2_dir_seek(&_lfs, d, offset); + _mutex.unlock(); +} + +off_t LittleFileSystem2::dir_tell(fs_dir_t dir) +{ + lfs2_dir_t *d = (lfs2_dir_t *)dir; + _mutex.lock(); + lfs2_soff_t res = lfs2_dir_tell(&_lfs, d); + _mutex.unlock(); + return lfs2_toerror(res); +} + +void LittleFileSystem2::dir_rewind(fs_dir_t dir) +{ + lfs2_dir_t *d = (lfs2_dir_t *)dir; + _mutex.lock(); + lfs2_dir_rewind(&_lfs, d); + _mutex.unlock(); +} + +} // namespace mbed diff --git a/features/storage/filesystem/littlefsv2/LittleFileSystem2.h b/features/storage/filesystem/littlefsv2/LittleFileSystem2.h new file mode 100644 index 00000000000..65aa3b18efb --- /dev/null +++ b/features/storage/filesystem/littlefsv2/LittleFileSystem2.h @@ -0,0 +1,308 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** \addtogroup storage */ +/** @{*/ + +#ifndef MBED_LFS2FILESYSTEM_H +#define MBED_LFS2FILESYSTEM_H + +#include "FileSystem.h" +#include "BlockDevice.h" +#include "PlatformMutex.h" +#include "lfs2.h" + +namespace mbed { + +/** + * LittleFileSystem2, a little file system + * + * Synchronization level: Thread safe + */ +class LittleFileSystem2 : public mbed::FileSystem { +public: + /** Lifetime of the LittleFileSystem2 + * + * @param name Name of the file system in the tree. + * @param bd Block device to mount. Mounted immediately if not NULL. + * @param block_size + * Size of a logical block. This does not impact ram consumption and + * may be larger than the physical erase block. If the physical erase + * block is larger, littlefs will use that instead. Larger values will + * be faster but waste more storage when files are not aligned to a + * block size. + * @param block_cycles + * Number of erase cycles before a block is forcefully evicted. Larger + * values are more efficient but cause less even wear distribution. 0 + * disables dynamic wear-leveling. + * @param cache_size + * Size of read/program caches. Each file uses 1 cache, and littlefs + * allocates 2 caches for internal operations. Larger values should be + * faster but uses more RAM. + * @param lookahead_size + * Size of the lookahead buffer. A larger lookahead reduces the + * allocation scans and results in a faster filesystem but uses + * more RAM. + */ + LittleFileSystem2(const char *name = NULL, mbed::BlockDevice *bd = NULL, + lfs2_size_t block_size = MBED_LFS2_BLOCK_SIZE, + uint32_t block_cycles = MBED_LFS2_BLOCK_CYCLES, + lfs2_size_t cache_size = MBED_LFS2_CACHE_SIZE, + lfs2_size_t lookahead = MBED_LFS2_LOOKAHEAD_SIZE); + + virtual ~LittleFileSystem2(); + + /** Format a block device with the LittleFileSystem2. + * + * The block device to format should be mounted when this function is called. + * + * @param bd This is the block device that will be formatted. + * @param block_size + * Size of a logical block. This does not impact ram consumption and + * may be larger than the physical erase block. If the physical erase + * block is larger, littlefs will use that instead. Larger values will + * be faster but waste more storage when files are not aligned to a + * block size. + * @param block_cycles + * Number of erase cycles before a block is forcefully evicted. Larger + * values are more efficient but cause less even wear distribution. 0 + * disables dynamic wear-leveling. + * @param cache_size + * Size of read/program caches. Each file uses 1 cache, and littlefs + * allocates 2 caches for internal operations. Larger values should be + * faster but uses more RAM. + * @param lookahead_size + * Size of the lookahead buffer. A larger lookahead reduces the + * allocation scans and results in a faster filesystem but uses + * more RAM. + */ + static int format(mbed::BlockDevice *bd, + lfs2_size_t block_size = MBED_LFS2_BLOCK_SIZE, + uint32_t block_cycles = MBED_LFS2_BLOCK_CYCLES, + lfs2_size_t cache_size = MBED_LFS2_CACHE_SIZE, + lfs2_size_t lookahead_size = MBED_LFS2_LOOKAHEAD_SIZE); + + /** Mount a file system to a block device. + * + * @param bd Block device to mount to. + * @return 0 on success, negative error code on failure. + */ + virtual int mount(mbed::BlockDevice *bd); + + /** Unmount a file system from the underlying block device. + * + * @return 0 on success, negative error code on failure + */ + virtual int unmount(); + + /** Reformat a file system. Results in an empty and mounted file system. + * + * @param bd + * Block device to reformat and mount. If NULL, the mounted + * Block device is used. + * Note: If mount fails, bd must be provided. + * Default: NULL + * + * @return 0 on success, negative error code on failure + */ + virtual int reformat(mbed::BlockDevice *bd); + + /** Remove a file from the file system. + * + * @param path The name of the file to remove. + * @return 0 on success, negative error code on failure + */ + virtual int remove(const char *path); + + /** Rename a file in the file system. + * + * @param path The name of the file to rename. + * @param newpath The name to rename it to. + * @return 0 on success, negative error code on failure + */ + virtual int rename(const char *path, const char *newpath); + + /** Store information about the file in a stat structure + * + * @param path The name of the file to find information about. + * @param st The stat buffer to write to. + * @return 0 on success, negative error code on failure + */ + virtual int stat(const char *path, struct stat *st); + + /** Create a directory in the file system. + * + * @param path The name of the directory to create. + * @param mode The permissions with which to create the directory. + * @return 0 on success, negative error code on failure + */ + virtual int mkdir(const char *path, mode_t mode); + + /** Store information about the mounted file system in a statvfs structure. + * + * @param path The name of the file to find information about. + * @param buf The stat buffer to write to. + * @return 0 on success, negative error code on failure + */ + virtual int statvfs(const char *path, struct statvfs *buf); + +protected: +#if !(DOXYGEN_ONLY) + /** Open a file on the file system. + * + * @param file Destination of the newly created handle to the referenced file. + * @param path The name of the file to open. + * @param flags The flags that trigger opening of the file. These flags are O_RDONLY, O_WRONLY, and O_RDWR, + * with an O_CREAT, O_TRUNC, or O_APPEND bitwise OR operator. + * @return 0 on success, negative error code on failure. + */ + virtual int file_open(mbed::fs_file_t *file, const char *path, int flags); + + /** Close a file + * + * @param file File handle. + * return 0 on success, negative error code on failure + */ + virtual int file_close(mbed::fs_file_t file); + + /** Read the contents of a file into a buffer + * + * @param file File handle. + * @param buffer The buffer to read in to. + * @param size The number of bytes to read. + * @return The number of bytes read, 0 at end of file, negative error on failure + */ + virtual ssize_t file_read(mbed::fs_file_t file, void *buffer, size_t size); + + /** Write the contents of a buffer to a file + * + * @param file File handle. + * @param buffer The buffer to write from. + * @param size The number of bytes to write. + * @return The number of bytes written, negative error on failure + */ + virtual ssize_t file_write(mbed::fs_file_t file, const void *buffer, size_t size); + + /** Flush any buffers associated with the file + * + * @param file File handle. + * @return 0 on success, negative error code on failure + */ + virtual int file_sync(mbed::fs_file_t file); + + /** Move the file position to a given offset from a given location + * + * @param file File handle. + * @param offset The offset from whence to move to. + * @param whence The start of where to seek. + * SEEK_SET to start from beginning of file, + * SEEK_CUR to start from current position in file, + * SEEK_END to start from end of file. + * @return The new offset of the file + */ + virtual off_t file_seek(mbed::fs_file_t file, off_t offset, int whence); + + /** Get the file position of the file + * + * @param file File handle. + * @return The current offset in the file + */ + virtual off_t file_tell(mbed::fs_file_t file); + + /** Get the size of the file + * + * @param file File handle. + * @return Size of the file in bytes + */ + virtual off_t file_size(mbed::fs_file_t file); + + /** Truncate or extend a file. + * + * The file's length is set to the specified value. The seek pointer is + * not changed. If the file is extended, the extended area appears as if + * it were zero-filled. + * + * @param file File handle. + * @param length The requested new length for the file + * + * @return Zero on success, negative error code on failure + */ + virtual int file_truncate(mbed::fs_file_t file, off_t length); + + /** Open a directory on the file system. + * + * @param dir Destination for the handle to the directory. + * @param path Name of the directory to open. + * @return 0 on success, negative error code on failure + */ + virtual int dir_open(mbed::fs_dir_t *dir, const char *path); + + /** Close a directory + * + * @param dir Dir handle. + * return 0 on success, negative error code on failure + */ + virtual int dir_close(mbed::fs_dir_t dir); + + /** Read the next directory entry + * + * @param dir Dir handle. + * @param ent The directory entry to fill out. + * @return 1 on reading a filename, 0 at end of directory, negative error on failure + */ + virtual ssize_t dir_read(mbed::fs_dir_t dir, struct dirent *ent); + + /** Set the current position of the directory + * + * @param dir Dir handle. + * @param offset Offset of the location to seek to, + * must be a value returned from dir_tell + */ + virtual void dir_seek(mbed::fs_dir_t dir, off_t offset); + + /** Get the current position of the directory + * + * @param dir Dir handle. + * @return Position of the directory that can be passed to dir_rewind + */ + virtual off_t dir_tell(mbed::fs_dir_t dir); + + /** Rewind the current position to the beginning of the directory + * + * @param dir Dir handle + */ + virtual void dir_rewind(mbed::fs_dir_t dir); +#endif //!(DOXYGEN_ONLY) + +private: + lfs2_t _lfs; // The actual file system + struct lfs2_config _config; + mbed::BlockDevice *_bd; // The block device + + // thread-safe locking + PlatformMutex _mutex; +}; + +} // namespace mbed + +// Added "using" for backwards compatibility +#ifndef MBED_NO_GLOBAL_USING_DIRECTIVE +using mbed::LittleFileSystem2; +#endif + +#endif + +/** @}*/ diff --git a/features/storage/filesystem/littlefsv2/README.md b/features/storage/filesystem/littlefsv2/README.md new file mode 100644 index 00000000000..eb4a0bebfbc --- /dev/null +++ b/features/storage/filesystem/littlefsv2/README.md @@ -0,0 +1,104 @@ +## Mbed OS API for the little filesystem + +This is the Mbed OS API for littlefs, a little fail-safe filesystem +designed for embedded systems. + +``` + | | | .---._____ + .-----. | | +--|o |---| littlefs | +--| |---| | + '-----' '----------' + | | | +``` + +**Bounded RAM/ROM** - The littlefs is designed to work with a limited amount +of memory. Recursion is avoided, and dynamic memory is limited to configurable +buffers that can be provided statically. + +**Power-loss resilient** - The littlefs is designed for systems that may have +random power failures. The littlefs has strong copy-on-write guarantees, and +storage on disk is always kept in a valid state. + +**Wear leveling** - Because the most common form of embedded storage is erodible +flash memories, littlefs provides a form of dynamic wear leveling for systems +that cannot fit a full flash translation layer. + +## Usage + +If you are already using a filesystem in Mbed, adopting the littlefs should +just require a name change to use the [LittleFileSystem2](LittleFileSystem2.h) +class. + +Here is a simple example that updates a file named "boot_count" every time +the application runs: +``` c++ +#include "LittleFileSystem2.h" +#include "SPIFBlockDevice.h" + +// Physical block device, can be any device that supports the BlockDevice API +SPIFBlockDevice bd(PTE2, PTE4, PTE1, PTE5); + +// Storage for the littlefs +LittleFileSystem2 fs("fs"); + +// Entry point +int main() { + // Mount the filesystem + int err = fs.mount(&bd); + if (err) { + // Reformat if we can't mount the filesystem, + // this should only happen on the first boot + LittleFileSystem2::format(&bd); + fs.mount(&bd); + } + + // Read the boot count + uint32_t boot_count = 0; + FILE *f = fopen("/fs/boot_count", "r+"); + if (!f) { + // Create the file if it doesn't exist + f = fopen("/fs/boot_count", "w+"); + } + fread(&boot_count, sizeof(boot_count), 1, f); + + // Update the boot count + boot_count += 1; + rewind(f); + fwrite(&boot_count, sizeof(boot_count), 1, f); + + // Remember that storage may not be updated until the file + // is closed successfully + fclose(f); + + // Release any resources we were using + fs.unmount(); + + // Print the boot count + printf("boot_count: %ld\n", boot_count); +} +``` + +## Reference material + +[DESIGN.md](littlefs/DESIGN.md) - DESIGN.md contains a fully detailed dive into +how littlefs actually works. We encourage you to read it because the +solutions and tradeoffs at work here are quite interesting. + +[SPEC.md](littlefs/SPEC.md) - SPEC.md contains the on-disk specification of +littlefs with all the nitty-gritty details. This can be useful for developing +tooling. + +## Related projects + +[littlefs](https://github.com/geky/littlefs) - Where the core of littlefs +currently lives. + +[littlefs-fuse](https://github.com/geky/littlefs-fuse) - A [FUSE](https://github.com/libfuse/libfuse) +wrapper for littlefs. The project allows you to mount littlefs directly in a +Linux machine. This can be useful for debugging littlefs if you have an SD card +handy. + +[littlefs-js](https://github.com/geky/littlefs-js) - A JavaScript wrapper for +littlefs. I'm not sure why you would want this, but it is handy for demos. +You can see it in action [here](http://littlefs.geky.net/demo.html). diff --git a/features/storage/filesystem/littlefsv2/TESTS/COMMON/atomic_usage.cpp b/features/storage/filesystem/littlefsv2/TESTS/COMMON/atomic_usage.cpp new file mode 100644 index 00000000000..dce4dc310a5 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/COMMON/atomic_usage.cpp @@ -0,0 +1,698 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017-2017 ARM Limited + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +/** + * This file contains code which performs various atomic operations using + * littlefs. It is intended for use in tests and test applications to + * validate that the defined behavior below is being met. + * + * # Defined behavior + * - A file rename is atomic (Note - rename can be used to replace a file) + * - Atomic file rename tested by setup/perform/check_file_rename + * - Atomic file replace tested by setup/perform/check_file_rename_replace + * - A directory rename is atomic (Note - rename can be used to replace an empty directory) + * - Tested by setup/perform/check_directory_rename + * - Directory create is atomic + * - Directory delete is atomic + * - File create is atomic + * - File delete is atomic + * - File contents are atomically written on close + * - Tested by setup/perform/check_file_change_contents + */ + + +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +#include "ObservingBlockDevice.h" +#include "ExhaustibleBlockDevice.h" +#include "FileSystem.h" + +#include "atomic_usage.h" + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) + + +#define DEBUG(...) +#define DEBUG_CHECK(...) +#define BUFFER_SIZE 64 +// Version is written to a file and is used +// to determine if a reformat is required +#define ATOMIC_USAGE_VERSION 1 + +#define ARRAY_LENGTH(array) (sizeof(array) / sizeof(array[0])) + +#define TEST_ASSERT_OR_EXIT(condition) \ + TEST_ASSERT(condition); if (!(condition)) {error("Assert failed");} + +#define TEST_ASSERT_EQUAL_OR_EXIT(expected, actual) \ + TEST_ASSERT_EQUAL(expected, actual); if ((int64_t)(expected) != (int64_t)(actual)) {error("Assert failed");} + +using namespace utest::v1; + +typedef void (*test_function_t)(FileSystem *fs); +typedef bool (*test_function_bool_t)(FileSystem *fs); + +struct TestEntry { + const char *name; + test_function_t setup; + test_function_bool_t perform; + test_function_t check; +}; + +/** + * Write data to the file while checking for error conditions + * + * @param file File to write to + * @param data Data to write + * @param size Size of data to write + * @return true if flash has been exhausted, false otherwise + */ +static bool file_write(File *file, uint8_t *data, uint32_t size) +{ + int res = file->write(data, size); + if (-ENOSPC == res) { + return true; + } + TEST_ASSERT_EQUAL_OR_EXIT(size, res); + return false; +} + +/** + * Write padding data of the given size + * + * @param file Pointer to the file to write to + * @param padding Value to pad + * @param size Size to pad + * @return true if flash has been exhausted, false otherwise + */ +static bool file_pad(File *file, char padding, uint32_t size) +{ + uint8_t buf[BUFFER_SIZE]; + memset(buf, padding, sizeof(buf)); + + while (size > 0) { + uint32_t write_size = sizeof(buf) <= size ? sizeof(buf) : size; + if (file_write(file, buf, write_size)) { + return true; + } + size -= write_size; + } + + return false; +} + +/* + * Similar to fscanf but uses and mbed file + * + * @param file File to scan from + * @param format Format string of values to read + * @return the number of arguments read + */ +static int file_scanf(File *file, const char *format, ...) +{ + uint8_t buf[BUFFER_SIZE]; + va_list args; + memset(buf, 0, sizeof(buf)); + + int res = file->read(buf, sizeof(buf) - 1); + TEST_ASSERT_OR_EXIT(res >= 0); + + va_start(args, format); + int count = vsscanf((char *)buf, format, args); + va_end(args); + TEST_ASSERT_OR_EXIT(count >= 0); + + return count; +} + +/* + * Similar to fprintf but uses and mbed file + * + * @param file File to print to + * @param format Format string of values to write + * @return size written to file or -1 on out of space + */ +static int file_printf(File *file, const char *format, ...) +{ + uint8_t buf[BUFFER_SIZE]; + va_list args; + va_start(args, format); + int size = vsprintf((char *)buf, format, args); + va_end(args); + TEST_ASSERT_OR_EXIT((size >= 0) && (size <= (int)sizeof(buf))); + + if (file_write(file, buf, size)) { + return -1; + } + + return size; +} + + +static const char FILE_RENAME_A[] = "file_to_rename_a.txt"; +static const char FILE_RENAME_B[] = "file_to_rename_b.txt"; +static const char FILE_RENAME_CONTENTS[] = "Test contents for the file to be renamed"; +static const int FILE_RENAME_LEN = strlen(FILE_RENAME_CONTENTS); + +/** + * Setup for the file rename test + * + * Create file FILE_RENAME_A with contents FILE_RENAME_CONTENTS. + */ +static void setup_file_rename(FileSystem *fs) +{ + DEBUG("setup_file_rename()\n"); + + File file; + + int res = file.open(fs, FILE_RENAME_A, O_WRONLY | O_CREAT); + DEBUG(" open result %i\n", res); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + res = file.write(FILE_RENAME_CONTENTS, FILE_RENAME_LEN); + DEBUG(" write result %i\n", res); + TEST_ASSERT_EQUAL_OR_EXIT(FILE_RENAME_LEN, res); +} + +/** + * Change the file name to either FILE_RENAME_A or FILE_RENAME_B + */ +static bool perform_file_rename(FileSystem *fs) +{ + DEBUG("perform_file_rename()\n"); + + struct stat st; + int res = fs->stat(FILE_RENAME_A, &st); + const char *src = (res == 0) ? FILE_RENAME_A : FILE_RENAME_B; + const char *dst = (res == 0) ? FILE_RENAME_B : FILE_RENAME_A; + + DEBUG(" stat result %i\n", res); + TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0)); + + DEBUG(" Renaming %s to %s\n", src, dst); + res = fs->rename(src, dst); + if (-ENOSPC == res) { + return true; + } + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + return false; +} + +/** + * Check that the file rename is in a good state + * + * Check that there is only one file and that file contains the correct + * contents. + * + * Allowed states: + * - File FILE_RENAME_A exists with contents and FILE_RENAME_B does not + * - File FILE_RENAME_B exists with contents and FILE_RENAME_A does not + * + */ +static void check_file_rename(FileSystem *fs) +{ + + int files = 0; + int valids = 0; + const char *const filenames[] = {FILE_RENAME_A, FILE_RENAME_B}; + + for (int i = 0; i < 2; i++) { + File file; + if (file.open(fs, filenames[i], O_RDONLY) == 0) { + uint8_t buf[BUFFER_SIZE]; + files++; + memset(buf, 0, sizeof(buf)); + int res = file.read(buf, FILE_RENAME_LEN); + if (res != FILE_RENAME_LEN) { + break; + } + if (memcmp(buf, FILE_RENAME_CONTENTS, FILE_RENAME_LEN) != 0) { + break; + } + valids++; + } + } + + TEST_ASSERT_EQUAL_OR_EXIT(1, files); + TEST_ASSERT_EQUAL_OR_EXIT(1, valids); +} + + +static const char FILE_RENAME_REPLACE[] = "rename_replace_file.txt"; +static const char FILE_RENAME_REPLACE_NEW[] = "new_rename_replace_file.txt"; +static const char FILE_RENAME_REPLACE_FMT[] = "file replace count: %lu\n"; + +/** + * Create the file FILE_RENAME_REPLACE with initial contents + * + * Create an write an initial count of 0 to the file. + */ +static void setup_file_rename_replace(FileSystem *fs) +{ + DEBUG("setup_file_rename_replace()\n"); + File file; + + // Write out initial count + + int res = file.open(fs, FILE_RENAME_REPLACE, O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + uint32_t count = 0; + uint8_t buf[BUFFER_SIZE]; + memset(buf, 0, sizeof(buf)); + const int length = sprintf((char *)buf, FILE_RENAME_REPLACE_FMT, count); + TEST_ASSERT_OR_EXIT(length > 0); + + res = file.write(buf, length); + DEBUG(" write result %i\n", res); + TEST_ASSERT_EQUAL_OR_EXIT(length, res); +} + +/** + * Atomically increment the count in FILE_RENAME_REPLACE using a rename + */ +bool perform_file_rename_replace(FileSystem *fs) +{ + DEBUG("perform_file_rename_replace()\n"); + File file; + + // Read in previous count + + int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + uint64_t count; + int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count); + TEST_ASSERT_EQUAL_OR_EXIT(1, args_read); + + res = file.close(); + if (-ENOSPC == res) { + return true; + } + TEST_ASSERT_EQUAL(0, res); + + // Write out new count + + count++; + + res = file.open(fs, FILE_RENAME_REPLACE_NEW, O_WRONLY | O_CREAT); + if (-ENOSPC == res) { + return true; + } + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + if (file_printf(&file, FILE_RENAME_REPLACE_FMT, count) <= 0) { + return true; + } + + res = file.close(); + if (-ENOSPC == res) { + return true; + } + TEST_ASSERT_EQUAL(0, res); + + // Rename file + + res = fs->rename(FILE_RENAME_REPLACE_NEW, FILE_RENAME_REPLACE); + if (-ENOSPC == res) { + return true; + } + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + DEBUG(" count %llu -> %llu\n", count - 1, count); + + return false; +} + +/** + * Check that FILE_RENAME_REPLACE always has a valid count + * + * Allowed states: + * - FILE_RENAME_REPLACE exists with valid contents + */ +static void check_file_rename_replace(FileSystem *fs) +{ + DEBUG_CHECK("check_file_rename_replace()\n"); + File file; + + // Read in previous count + + int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + uint64_t count; + int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count); + TEST_ASSERT_EQUAL_OR_EXIT(1, args_read); + DEBUG_CHECK(" count %llu\n", count); +} + + +static const char DIRECTORY_RENAME_A[] = "dir_a"; +static const char DIRECTORY_RENAME_B[] = "dir_b"; + +/** + * Create DIRECTORY_RENAME_A with initial contents + */ +static void setup_directory_rename(FileSystem *fs) +{ + DEBUG("setup_directory_rename()\n"); + + int res = fs->mkdir(DIRECTORY_RENAME_A, 0777); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); +} + +/* + * Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other + */ +static bool perform_directory_rename(FileSystem *fs) +{ + DEBUG("perform_directory_rename()\n"); + + struct stat st; + int res = fs->stat(DIRECTORY_RENAME_A, &st); + const char *src = (res == 0) ? DIRECTORY_RENAME_A : DIRECTORY_RENAME_B; + const char *dst = (res == 0) ? DIRECTORY_RENAME_B : DIRECTORY_RENAME_A; + + DEBUG(" stat result %i\n", res); + TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0)); + + DEBUG(" Renaming %s to %s\n", src, dst); + res = fs->rename(src, dst); + if (-ENOSPC == res) { + return true; + } + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + return false; +} + +/* + * Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other + * + * Allowed states: + * - DIRECTORY_RENAME_A exists with valid contents and DIRECTORY_RENAME_B does not exist + * - DIRECTORY_RENAME_B exists with valid contents and DIRECTORY_RENAME_A does not exist + */ +static void check_directory_rename(FileSystem *fs) +{ + DEBUG_CHECK("check_directory_rename()\n"); + + static const char *directory_names[] = { + DIRECTORY_RENAME_A, + DIRECTORY_RENAME_B + }; + + size_t directories = 0; + for (size_t i = 0; i < ARRAY_LENGTH(directory_names); i++) { + Dir dir; + int res = dir.open(fs, directory_names[i]); + TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0)); + if (res == 0) { + directories++; + } + } + TEST_ASSERT_EQUAL_OR_EXIT(1, directories); +} + + +static const char CHANGE_CONTENTS_NAME[] = "file_changing_contents.txt"; +static const char CHANGE_CONTENTS_FILL = ' '; +static const uint32_t BLOCK_SIZE = 512; + +/** + * Create file CHANGE_CONTENTS_NAME with initial contents + * + * File contains three blocks of data each which start + * with a count. + */ +static void setup_file_change_contents(FileSystem *fs) +{ + DEBUG("setup_file_change_contents()\n"); + + File file; + int res = file.open(fs, CHANGE_CONTENTS_NAME, O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + for (int count = 1; count <= 3; count++) { + int size = file_printf(&file, "%lu\n", count); + TEST_ASSERT_OR_EXIT(size >= 0); + + bool dead = file_pad(&file, CHANGE_CONTENTS_FILL, BLOCK_SIZE - size); + TEST_ASSERT_EQUAL_OR_EXIT(false, dead); + } +} + +/** + * Atomically increment the counts in the file CHANGE_CONTENTS_NAME + * + * Read in the current counts, increment them and then write them + * back in non-sequential order. + */ +static bool perform_file_change_contents(FileSystem *fs) +{ + DEBUG("perform_file_change_contents()\n"); + File file; + + int res = file.open(fs, CHANGE_CONTENTS_NAME, O_RDWR); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + // Read in values + uint32_t values[3]; + for (int i = 0; i < 3; i++) { + file.seek(i * BLOCK_SIZE); + int args_read = file_scanf(&file, "%lu\n", &values[i]); + TEST_ASSERT_EQUAL_OR_EXIT(1, args_read); + } + + // Increment values + for (int i = 0; i < 3; i++) { + values[i]++; + } + + // Write values out of order + int i; + i = 0; + file.seek(i * BLOCK_SIZE); + if (file_printf(&file, "%lu\n", values[i]) <= 0) { + return true; + } + DEBUG(" value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]); + + i = 2; + file.seek(i * BLOCK_SIZE); + if (file_printf(&file, "%lu\n", values[i]) <= 0) { + return true; + } + DEBUG(" value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]); + + i = 1; + file.seek(i * BLOCK_SIZE); + if (file_printf(&file, "%lu\n", values[i]) <= 0) { + return true; + } + DEBUG(" value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]); + + res = file.close(); + if (-ENOSPC == res) { + return true; + } + TEST_ASSERT_EQUAL(0, res); + + return false; +} + +/* + * Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other + * + * Allowed states: + * - CHANGE_CONTENTS_NAME exists and contains 3 counts which are in order + */ +static void check_file_change_contents(FileSystem *fs) +{ + DEBUG_CHECK("check_file_change_contents()\n"); + File file; + + int res = file.open(fs, CHANGE_CONTENTS_NAME, O_RDONLY); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + // Read in values + uint32_t values[3]; + for (int i = 0; i < 3; i++) { + file.seek(i * BLOCK_SIZE); + int args_read = file_scanf(&file, "%lu\n", &values[i]); + TEST_ASSERT_EQUAL_OR_EXIT(1, args_read); + DEBUG_CHECK(" value[%i]: %lu\n", i, values[i]); + } + + TEST_ASSERT_EQUAL_OR_EXIT(values[0] + 1, values[1]); + TEST_ASSERT_EQUAL_OR_EXIT(values[1] + 1, values[2]); +} + + +static const TestEntry atomic_test_entries[] = { + {"File rename", setup_file_rename, perform_file_rename, check_file_rename}, + {"File rename replace", setup_file_rename_replace, perform_file_rename_replace, check_file_rename_replace}, + {"Directory rename", setup_directory_rename, perform_directory_rename, check_directory_rename}, + {"File change contents", setup_file_change_contents, perform_file_change_contents, check_file_change_contents}, +}; + +static const char FILE_SETUP_COMPLETE[] = "setup_complete.txt"; +static const char FILE_SETUP_COMPLETE_FMT[] = "Test version: %lu\n"; + +static bool format_required(BlockDevice *bd) +{ + MBED_TEST_FILESYSTEM_DECL; + + if (fs.mount(bd) != 0) { + return true; + } + + // Check if setup complete file exists + File file; + int res = file.open(&fs, FILE_SETUP_COMPLETE, O_RDONLY); + if (res != 0) { + return true; + } + + // Read contents of setup complete file + uint8_t buf[BUFFER_SIZE]; + memset(buf, 0, sizeof(buf)); + int size_read = file.read(buf, sizeof(buf) - 1); + if (size_read <= 0) { + return true; + } + + // Get the test version + uint32_t version = 0; + res = sscanf((char *)buf, FILE_SETUP_COMPLETE_FMT, &version); + if (res != 1) { + return true; + } + + if (ATOMIC_USAGE_VERSION != version) { + return true; + } + + // Setup file exists and is the correct version + return false; +} + +static void format(BlockDevice *bd) +{ + MBED_TEST_FILESYSTEM_DECL; + + int res = fs.format(bd); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + res = fs.mount(bd); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) { + atomic_test_entries[i].setup(&fs); + } + + File file; + res = file.open(&fs, FILE_SETUP_COMPLETE, O_CREAT | O_WRONLY); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + int size = file_printf(&file, FILE_SETUP_COMPLETE_FMT, (uint32_t)ATOMIC_USAGE_VERSION); + TEST_ASSERT_OR_EXIT(size >= 0); +} + +static int64_t get_cycle_count(FileSystem *fs) +{ + File file; + + int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY); + TEST_ASSERT_EQUAL_OR_EXIT(0, res); + + uint64_t count = 0; + int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count); + TEST_ASSERT_EQUAL_OR_EXIT(1, args_read); + + file.close(); + + return (int64_t)count; +} + +bool setup_atomic_operations(BlockDevice *bd, bool force_rebuild) +{ + if (force_rebuild || format_required(bd)) { + format(bd); + TEST_ASSERT_EQUAL_OR_EXIT(false, format_required(bd)); + return true; + } + return false; +} + +int64_t perform_atomic_operations(BlockDevice *bd) +{ + MBED_TEST_FILESYSTEM_DECL; + bool out_of_space = false; + + fs.mount(bd); + + for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) { + out_of_space |= atomic_test_entries[i].perform(&fs); + } + + int64_t cycle_count = get_cycle_count(&fs); + + fs.unmount(); + + if (out_of_space) { + return -1; + } else { + return cycle_count; + } +} + +void check_atomic_operations(BlockDevice *bd) +{ + MBED_TEST_FILESYSTEM_DECL; + fs.mount(bd); + + for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) { + atomic_test_entries[i].check(&fs); + } + + fs.unmount(); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/COMMON/atomic_usage.h b/features/storage/filesystem/littlefsv2/TESTS/COMMON/atomic_usage.h new file mode 100644 index 00000000000..43bf5be94a6 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/COMMON/atomic_usage.h @@ -0,0 +1,71 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef MBED_ATOMIC_USAGE_H +#define MBED_ATOMIC_USAGE_H + +#include "BlockDevice.h" + +/** + * Setup the given block device to test littlefs atomic operations + * + * Format the blockdevice with a littlefs filesystem and create + * the files and directories required to test atomic operations. + * + * @param bd Block device format and setup + * @param force_rebuild Force a reformat even if the device is already setup + * @return true if the block device was formatted, false otherwise + * @note utest asserts are used to detect fatal errors so utest must be + * initialized before calling this function. + */ +bool setup_atomic_operations(BlockDevice *bd, bool force_rebuild); + +/** + * Perform a set of atomic littlefs operations on the block device + * + * Mount the block device as a littlefs filesystem and a series of + * atomic operations on it. Since the operations performed are atomic + * the file system will always be in a well defined state. The block + * device must have been setup by calling setup_atomic_operations. + * + * @param bd Block device to perform the operations on + * @return -1 if flash is exhausted, otherwise the cycle count on the fs + * @note utest asserts are used to detect fatal errors so utest must be + * initialized before calling this function. + */ +int64_t perform_atomic_operations(BlockDevice *bd); + +/** + * Check that the littlefs image on the block device is in a good state + * + * Mount the block device as a littlefs filesystem and check the files + * and directories to ensure they are valid. Since all the operations + * performed are atomic the filesystem should always be in a good + * state. + * + * @param bd Block device to check + * @note This function does not change the contents of the block device + * @note utest asserts are used to detect fatal errors so utest must be + * initialized before calling this function. + */ +void check_atomic_operations(BlockDevice *bd); + +#endif diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem/dirs/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem/dirs/main.cpp new file mode 100644 index 00000000000..647a3a78032 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem/dirs/main.cpp @@ -0,0 +1,683 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests + +void test_directory_tests() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_root_directory() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "/"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_creation() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("potato", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_file_creation() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "burito", O_CREAT | O_WRONLY); + TEST_ASSERT_EQUAL(0, res); + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_iteration() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "/"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "burito"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "potato"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_failures() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("potato", 0777); + TEST_ASSERT_EQUAL(-EEXIST, res); + res = dir[0].open(&fs, "tomato"); + TEST_ASSERT_EQUAL(-ENOENT, res); + res = dir[0].open(&fs, "burito"); + TEST_ASSERT_EQUAL(-ENOTDIR, res); + res = file[0].open(&fs, "tomato", O_RDONLY); + TEST_ASSERT_EQUAL(-ENOENT, res); + res = file[0].open(&fs, "potato", O_RDONLY); + TEST_ASSERT_EQUAL(-EISDIR, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_nested_directories() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("potato/baked", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("potato/sweet", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("potato/fried", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "potato"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "baked"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "fried"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "sweet"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_multi_block_directory() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("cactus", 0777); + TEST_ASSERT_EQUAL(0, res); + for (int i = 0; i < 128; i++) { + sprintf((char *)buffer, "cactus/test%03d", i); + res = fs.mkdir((char *)buffer, 0777); + TEST_ASSERT_EQUAL(0, res); + } + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "cactus"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + for (int i = 0; i < 128; i++) { + sprintf((char *)buffer, "test%03d", i); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + } + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_remove() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.remove("potato"); + TEST_ASSERT_EQUAL(-ENOTEMPTY, res); + res = fs.remove("potato/sweet"); + TEST_ASSERT_EQUAL(0, res); + res = fs.remove("potato/baked"); + TEST_ASSERT_EQUAL(0, res); + res = fs.remove("potato/fried"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "potato"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.remove("potato"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "/"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "burito"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "cactus"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "/"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "burito"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "cactus"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_rename() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("coldpotato", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("coldpotato/baked", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("coldpotato/sweet", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("coldpotato/fried", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.rename("coldpotato", "hotpotato"); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "hotpotato"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "baked"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "fried"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "sweet"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("warmpotato", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("warmpotato/mushy", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.rename("hotpotato", "warmpotato"); + TEST_ASSERT_EQUAL(-ENOTEMPTY, res); + res = fs.remove("warmpotato/mushy"); + TEST_ASSERT_EQUAL(0, res); + res = fs.rename("hotpotato", "warmpotato"); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "warmpotato"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "baked"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "fried"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "sweet"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("coldpotato", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.rename("warmpotato/baked", "coldpotato/baked"); + TEST_ASSERT_EQUAL(0, res); + res = fs.rename("warmpotato/sweet", "coldpotato/sweet"); + TEST_ASSERT_EQUAL(0, res); + res = fs.rename("warmpotato/fried", "coldpotato/fried"); + TEST_ASSERT_EQUAL(0, res); + res = fs.remove("coldpotato"); + TEST_ASSERT_EQUAL(-ENOTEMPTY, res); + res = fs.remove("warmpotato"); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "coldpotato"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "baked"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "fried"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "sweet"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("Directory tests", test_directory_tests), + Case("Root directory", test_root_directory), + Case("Directory creation", test_directory_creation), + Case("File creation", test_file_creation), + Case("Directory iteration", test_directory_iteration), + Case("Directory failures", test_directory_failures), + Case("Nested directories", test_nested_directories), + Case("Multi-block directory", test_multi_block_directory), + Case("Directory remove", test_directory_remove), + Case("Directory rename", test_directory_rename), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem/files/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem/files/main.cpp new file mode 100644 index 00000000000..51d1a057bd5 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem/files/main.cpp @@ -0,0 +1,449 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests + +void test_file_tests() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_simple_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "hello", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + size = strlen("Hello World!\n"); + memcpy(wbuffer, "Hello World!\n", size); + res = file[0].write(wbuffer, size); + TEST_ASSERT_EQUAL(size, res); + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "hello", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + size = strlen("Hello World!\n"); + res = file[0].read(rbuffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(rbuffer, wbuffer, size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_small_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + size_t size = 32; + size_t chunk = 31; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "smallavacado", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + for (size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + res = file[0].write(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 32; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "smallavacado", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = file[0].read(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_medium_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + size_t size = 8192; + size_t chunk = 31; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "mediumavacado", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + for (size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + res = file[0].write(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 8192; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "mediumavacado", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = file[0].read(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_large_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + size_t size = 262144; + size_t chunk = 31; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "largeavacado", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + for (size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + res = file[0].write(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 262144; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "largeavacado", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = file[0].read(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_non_overlap_check() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + size_t size = 32; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "smallavacado", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = file[0].read(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 8192; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "mediumavacado", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = file[0].read(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 262144; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "largeavacado", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = file[0].read(buffer, chunk); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_dir_check() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "/"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "hello"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "largeavacado"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "mediumavacado"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "smallavacado"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("File tests", test_file_tests), + Case("Simple file test", test_simple_file_test), + Case("Small file test", test_small_file_test), + Case("Medium file test", test_medium_file_test), + Case("Large file test", test_large_file_test), + Case("Non-overlap check", test_non_overlap_check), + Case("Dir check", test_dir_check), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem/interspersed/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem/interspersed/main.cpp new file mode 100644 index 00000000000..9772ddf6af8 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem/interspersed/main.cpp @@ -0,0 +1,407 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests + +void test_parallel_tests() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_parallel_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "a", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + res = file[1].open(&fs, "b", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + res = file[2].open(&fs, "c", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + res = file[3].open(&fs, "d", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 10; i++) { + res = file[0].write((const void *)"a", 1); + TEST_ASSERT_EQUAL(1, res); + res = file[1].write((const void *)"b", 1); + TEST_ASSERT_EQUAL(1, res); + res = file[2].write((const void *)"c", 1); + TEST_ASSERT_EQUAL(1, res); + res = file[3].write((const void *)"d", 1); + TEST_ASSERT_EQUAL(1, res); + } + + file[0].close(); + file[1].close(); + file[2].close(); + file[3].close(); + res = dir[0].open(&fs, "/"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "a"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "b"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "c"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "d"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "a", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + res = file[1].open(&fs, "b", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + res = file[2].open(&fs, "c", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + res = file[3].open(&fs, "d", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 10; i++) { + res = file[0].read(buffer, 1); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('a', res); + res = file[1].read(buffer, 1); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('b', res); + res = file[2].read(buffer, 1); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('c', res); + res = file[3].read(buffer, 1); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('d', res); + } + + file[0].close(); + file[1].close(); + file[2].close(); + file[3].close(); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_parallel_remove_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "e", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 5; i++) { + res = file[0].write((const void *)"e", 1); + TEST_ASSERT_EQUAL(1, res); + } + res = fs.remove("a"); + TEST_ASSERT_EQUAL(0, res); + res = fs.remove("b"); + TEST_ASSERT_EQUAL(0, res); + res = fs.remove("c"); + TEST_ASSERT_EQUAL(0, res); + res = fs.remove("d"); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 5; i++) { + res = file[0].write((const void *)"e", 1); + TEST_ASSERT_EQUAL(1, res); + } + + file[0].close(); + res = dir[0].open(&fs, "/"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "e"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "e", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 10; i++) { + res = file[0].read(buffer, 1); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('e', res); + } + + file[0].close(); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_remove_inconveniently_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "e", O_WRONLY | O_TRUNC); + TEST_ASSERT_EQUAL(0, res); + res = file[1].open(&fs, "f", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + res = file[2].open(&fs, "g", O_WRONLY | O_CREAT); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 5; i++) { + res = file[0].write((const void *)"e", 1); + TEST_ASSERT_EQUAL(1, res); + res = file[1].write((const void *)"f", 1); + TEST_ASSERT_EQUAL(1, res); + res = file[2].write((const void *)"g", 1); + TEST_ASSERT_EQUAL(1, res); + } + res = fs.remove("f"); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 5; i++) { + res = file[0].write((const void *)"e", 1); + TEST_ASSERT_EQUAL(1, res); + res = file[1].write((const void *)"f", 1); + TEST_ASSERT_EQUAL(1, res); + res = file[2].write((const void *)"g", 1); + TEST_ASSERT_EQUAL(1, res); + } + + file[0].close(); + file[1].close(); + file[2].close(); + res = dir[0].open(&fs, "/"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "e"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "g"); + TEST_ASSERT_EQUAL(0, res); + res = ent.d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "e", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + res = file[1].open(&fs, "g", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 10; i++) { + res = file[0].read(buffer, 1); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('e', res); + res = file[1].read(buffer, 1); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('g', res); + } + + file[0].close(); + file[1].close(); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("Parallel tests", test_parallel_tests), + Case("Parallel file test", test_parallel_file_test), + Case("Parallel remove file test", test_parallel_remove_file_test), + Case("Remove inconveniently test", test_remove_inconveniently_test), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem/seek/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem/seek/main.cpp new file mode 100644 index 00000000000..2d114b0fdfe --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem/seek/main.cpp @@ -0,0 +1,643 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests + +void test_seek_tests() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mkdir("hello", 0777); + TEST_ASSERT_EQUAL(0, res); + for (int i = 0; i < 132; i++) { + sprintf((char *)buffer, "hello/kitty%03d", i); + res = file[0].open(&fs, (char *)buffer, + O_WRONLY | O_CREAT | O_APPEND); + TEST_ASSERT_EQUAL(0, res); + + size = strlen("kittycatcat"); + memcpy(buffer, "kittycatcat", size); + for (int j = 0; j < 132; j++) { + file[0].write(buffer, size); + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + } + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_simple_dir_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "hello"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + int i; + for (i = 0; i < 4; i++) { + sprintf((char *)buffer, "kitty%03d", i); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + pos = dir[0].tell(); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + + dir[0].seek(pos); + sprintf((char *)buffer, "kitty%03d", i); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + + dir[0].rewind(); + sprintf((char *)buffer, "kitty%03d", 0); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + + dir[0].seek(pos); + sprintf((char *)buffer, "kitty%03d", i); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_large_dir_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].open(&fs, "hello"); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + int i; + for (i = 0; i < 128; i++) { + sprintf((char *)buffer, "kitty%03d", i); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + pos = dir[0].tell(); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + + dir[0].seek(pos); + sprintf((char *)buffer, "kitty%03d", i); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + + dir[0].rewind(); + sprintf((char *)buffer, "kitty%03d", 0); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + + dir[0].seek(pos); + sprintf((char *)buffer, "kitty%03d", i); + res = dir[0].read(&ent); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ent.d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + res = dir[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_simple_file_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "hello/kitty042", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + size = strlen("kittycatcat"); + for (int i = 0; i < 4; i++) { + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + pos = file[0].tell(); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + file[0].rewind(); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(-size, SEEK_CUR); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(-size, SEEK_END) >= 0; + TEST_ASSERT_EQUAL(1, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + size_t size = file[0].size(); + res = file[0].seek(0, SEEK_CUR); + TEST_ASSERT_EQUAL(size, res); + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_large_file_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "hello/kitty042", O_RDONLY); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + size = strlen("kittycatcat"); + for (int i = 0; i < 128; i++) { + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + pos = file[0].tell(); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + file[0].rewind(); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(-size, SEEK_CUR); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(-size, SEEK_END) >= 0; + TEST_ASSERT_EQUAL(1, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + size_t size = file[0].size(); + res = file[0].seek(0, SEEK_CUR); + TEST_ASSERT_EQUAL(size, res); + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_simple_file_seek_and_write() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "hello/kitty042", O_RDWR); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + size = strlen("kittycatcat"); + for (int i = 0; i < 4; i++) { + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + pos = file[0].tell(); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + + memcpy(buffer, "doggodogdog", size); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].write(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "doggodogdog", size); + TEST_ASSERT_EQUAL(0, res); + + file[0].rewind(); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "doggodogdog", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(-size, SEEK_END) >= 0; + TEST_ASSERT_EQUAL(1, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + size_t size = file[0].size(); + res = file[0].seek(0, SEEK_CUR); + TEST_ASSERT_EQUAL(size, res); + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_large_file_seek_and_write() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "hello/kitty042", O_RDWR); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + size = strlen("kittycatcat"); + for (int i = 0; i < 128; i++) { + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + if (i != 4) { + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + } + pos = file[0].tell(); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + + memcpy(buffer, "doggodogdog", size); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].write(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "doggodogdog", size); + TEST_ASSERT_EQUAL(0, res); + + file[0].rewind(); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(pos, SEEK_SET); + TEST_ASSERT_EQUAL(pos, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "doggodogdog", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(-size, SEEK_END) >= 0; + TEST_ASSERT_EQUAL(1, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + size_t size = file[0].size(); + res = file[0].seek(0, SEEK_CUR); + TEST_ASSERT_EQUAL(size, res); + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_boundary_seek_and_write() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "hello/kitty042", O_RDWR); + TEST_ASSERT_EQUAL(0, res); + + size = strlen("hedgehoghog"); + const off_t offsets[] = {512, 1020, 513, 1021, 511, 1019}; + + for (int i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) { + off_t off = offsets[i]; + memcpy(buffer, "hedgehoghog", size); + res = file[0].seek(off, SEEK_SET); + TEST_ASSERT_EQUAL(off, res); + res = file[0].write(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = file[0].seek(off, SEEK_SET); + TEST_ASSERT_EQUAL(off, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "hedgehoghog", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(0, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].sync(); + TEST_ASSERT_EQUAL(0, res); + } + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_out_of_bounds_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = file[0].open(&fs, "hello/kitty042", O_RDWR); + TEST_ASSERT_EQUAL(0, res); + + size = strlen("kittycatcat"); + res = file[0].size(); + TEST_ASSERT_EQUAL(132 * size, res); + res = file[0].seek((132 + 4) * size, + SEEK_SET); + TEST_ASSERT_EQUAL((132 + 4)*size, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(0, res); + + memcpy(buffer, "porcupineee", size); + res = file[0].write(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = file[0].seek((132 + 4) * size, + SEEK_SET); + TEST_ASSERT_EQUAL((132 + 4)*size, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "porcupineee", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].seek(132 * size, + SEEK_SET); + TEST_ASSERT_EQUAL(132 * size, res); + res = file[0].read(buffer, size); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "\0\0\0\0\0\0\0\0\0\0\0", size); + TEST_ASSERT_EQUAL(0, res); + res = file[0].close(); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("Seek tests", test_seek_tests), + Case("Simple dir seek", test_simple_dir_seek), + Case("Large dir seek", test_large_dir_seek), + Case("Simple file seek", test_simple_file_seek), + Case("Large file seek", test_large_file_seek), + Case("Simple file seek and write", test_simple_file_seek_and_write), + Case("Large file seek and write", test_large_file_seek_and_write), + Case("Boundary seek and write", test_boundary_seek_and_write), + Case("Out-of-bounds seek", test_out_of_bounds_seek), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem_integration/format/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem_integration/format/main.cpp new file mode 100644 index 00000000000..390d532d7ed --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem_integration/format/main.cpp @@ -0,0 +1,199 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests for integration level format operations + +void test_format() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_mount() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_bad_mount() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = bd.erase(0, 2 * bd.get_erase_size()); + TEST_ASSERT_EQUAL(0, res); + memset(buffer, 0, bd.get_program_size()); + for (int i = 0; i < 2 * bd.get_erase_size(); i += bd.get_program_size()) { + res = bd.program(buffer, i, bd.get_program_size()); + TEST_ASSERT_EQUAL(0, res); + } + } + + { + res = fs.mount(&bd); + TEST_ASSERT_NOT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_bad_mount_then_reformat() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_NOT_EQUAL(0, res); + + res = fs.reformat(&bd); + TEST_ASSERT_EQUAL(0, res); + + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_good_mount_then_reformat() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + + res = fs.reformat(&bd); + TEST_ASSERT_EQUAL(0, res); + + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("Test format", test_format), + Case("Test mount", test_mount), + Case("Test bad mount", test_bad_mount), + Case("Test bad mount than reformat", test_bad_mount_then_reformat), + Case("Test good mount than reformat", test_good_mount_then_reformat), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/resilience/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/resilience/main.cpp new file mode 100644 index 00000000000..802fb65a841 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/resilience/main.cpp @@ -0,0 +1,104 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "unity.h" +#include "utest.h" +#include "test_env.h" + +#include "atomic_usage.h" +#include "ObservingBlockDevice.h" + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_SIM_BLOCKDEVICE +#error [NOT_SUPPORTED] Simulation block device required for resilience tests +#endif + +#ifndef MBED_TEST_SIM_BLOCKDEVICE_DECL +#define MBED_TEST_SIM_BLOCKDEVICE_DECL MBED_TEST_SIM_BLOCKDEVICE bd(MBED_TEST_BLOCK_COUNT*512, 1, 1, 512) +#endif + +#ifndef MBED_TEST_BLOCK_COUNT +#define MBED_TEST_BLOCK_COUNT 64 +#endif + +#ifndef MBED_TEST_CYCLES +#define MBED_TEST_CYCLES 10 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_SIM_BLOCKDEVICE) + + +/** + * Check that the filesystem is valid after every change + * + * This test is to ensure that littlefs contains a valid filesystem at + * all times. This property is required for handling unexpected power + * loss. + */ +void test_resilience() +{ + MBED_TEST_SIM_BLOCKDEVICE_DECL; + + // bring up to get block size + bd.init(); + bd_size_t block_size = bd.get_erase_size(); + bd.deinit(); + + SlicingBlockDevice slice(&bd, 0, MBED_TEST_BLOCK_COUNT * block_size); + + // Setup the test + setup_atomic_operations(&slice, true); + + // Run check on every write operation + ObservingBlockDevice observer(&slice); + observer.attach(check_atomic_operations); + + // Perform operations + printf("Performing %i operations on flash\n", MBED_TEST_CYCLES); + for (int i = 1; i <= MBED_TEST_CYCLES; i++) { + int64_t ret = perform_atomic_operations(&observer); + TEST_ASSERT_EQUAL(i, ret); + } + printf("No errors detected\n"); +} + +Case cases[] = { + Case("test resilience", test_resilience), +}; + +utest::v1::status_t greentea_test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return greentea_test_setup_handler(number_of_cases); +} + +Specification specification(greentea_test_setup, cases, greentea_test_teardown_handler); + +int main() +{ + Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/resilience_functional/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/resilience_functional/main.cpp new file mode 100644 index 00000000000..aa574a6e902 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/resilience_functional/main.cpp @@ -0,0 +1,119 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "unity.h" +#include "utest.h" +#include "test_env.h" + +#include "atomic_usage.h" +#include "ObservingBlockDevice.h" +#include "LittleFileSystem2.h" + +#include + +using namespace utest::v1; + + +// test configuration +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required for resilience_functional tests +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_BLOCK_COUNT +#define MBED_TEST_BLOCK_COUNT 64 +#endif + +#ifndef MBED_TEST_CYCLES +#define MBED_TEST_CYCLES 10 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_BLOCKDEVICE_DECL; + +typedef enum { + CMD_STATUS_PASS, + CMD_STATUS_FAIL, + CMD_STATUS_CONTINUE, + CMD_STATUS_ERROR +} cmd_status_t; + +void use_filesystem() +{ + // Perform operations + while (true) { + int64_t ret = perform_atomic_operations(&bd); + TEST_ASSERT(ret > 0); + } +} + +static cmd_status_t handle_command(const char *key, const char *value) +{ + if (strcmp(key, "format") == 0) { + setup_atomic_operations(&bd, true); + greentea_send_kv("format_done", 1); + return CMD_STATUS_CONTINUE; + + } else if (strcmp(key, "run") == 0) { + use_filesystem(); + return CMD_STATUS_CONTINUE; + + } else if (strcmp(key, "exit") == 0) { + if (strcmp(value, "pass") != 0) { + return CMD_STATUS_FAIL; + } + check_atomic_operations(&bd); + return CMD_STATUS_PASS; + + } else { + return CMD_STATUS_ERROR; + + } +} + +int main() +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "unexpected_reset"); + + static char _key[10 + 1] = {}; + static char _value[128 + 1] = {}; + + greentea_send_kv("start", 1); + + // Handshake with host + cmd_status_t cmd_status = CMD_STATUS_CONTINUE; + while (CMD_STATUS_CONTINUE == cmd_status) { + memset(_key, 0, sizeof(_key)); + memset(_value, 0, sizeof(_value)); + greentea_parse_kv(_key, _value, sizeof(_key) - 1, sizeof(_value) - 1); + cmd_status = handle_command(_key, _value); + } + + GREENTEA_TESTSUITE_RESULT(CMD_STATUS_PASS == cmd_status); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/wear_leveling/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/wear_leveling/main.cpp new file mode 100644 index 00000000000..a9b8973ce89 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem_recovery/wear_leveling/main.cpp @@ -0,0 +1,120 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "unity.h" +#include "utest.h" +#include "test_env.h" + +#include "atomic_usage.h" +#include "ExhaustibleBlockDevice.h" +#include "SlicingBlockDevice.h" + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_SIM_BLOCKDEVICE +#error [NOT_SUPPORTED] Simulation block device required for wear leveling tests +#endif + +#ifndef MBED_TEST_SIM_BLOCKDEVICE_DECL +#define MBED_TEST_SIM_BLOCKDEVICE_DECL MBED_TEST_SIM_BLOCKDEVICE bd(MBED_TEST_BLOCK_COUNT*512, 1, 1, 512) +#endif + +#ifndef MBED_TEST_BLOCK_COUNT +#define MBED_TEST_BLOCK_COUNT 64 +#endif + +#ifndef MBED_TEST_ERASE_CYCLES +#define MBED_TEST_ERASE_CYCLES 100 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_SIM_BLOCKDEVICE) + + +static uint32_t test_wear_leveling_size(uint32_t block_count) +{ + MBED_TEST_SIM_BLOCKDEVICE_DECL; + + // bring up to get block size + bd.init(); + bd_size_t block_size = bd.get_erase_size(); + bd.deinit(); + + SlicingBlockDevice slice(&bd, 0, block_count * block_size); + ExhaustibleBlockDevice ebd(&slice, MBED_TEST_ERASE_CYCLES); + + printf("Testing size %llu bytes (%lux%llu) blocks\n", + block_count * block_size, block_count, block_size); + setup_atomic_operations(&ebd, true); + + int64_t cycles = 0; + while (true) { + int64_t ret = perform_atomic_operations(&ebd); + check_atomic_operations(&ebd); + if (-1 == ret) { + break; + } + cycles++; + TEST_ASSERT_EQUAL(cycles, ret); + + } + + printf(" Simulated flash lasted %lli cylces\n", cycles); + return cycles; +} + +/** + * Check that storage life is proportional to storage size + * + * This test is to ensure that littlefs is properly handling wear + * leveling. It does this by creating a simulated flash block device + * which can be worn out and then using it until it is exhausted. + * It then doubles the size of the block device and runs the same + * test. If the block device with twice the size lasts at least + * twice as long then the test passes. + */ +void test_wear_leveling() +{ + uint32_t cycles_1 = test_wear_leveling_size(MBED_TEST_BLOCK_COUNT / 2); + uint32_t cycles_2 = test_wear_leveling_size(MBED_TEST_BLOCK_COUNT / 1); + TEST_ASSERT(cycles_2 > cycles_1 * 2); +} + +Case cases[] = { + Case("test wear leveling", test_wear_leveling), +}; + +utest::v1::status_t greentea_test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return greentea_test_setup_handler(number_of_cases); +} + +Specification specification(greentea_test_setup, cases, greentea_test_teardown_handler); + +int main() +{ + Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/dirs/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/dirs/main.cpp new file mode 100644 index 00000000000..9761d1349ba --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/dirs/main.cpp @@ -0,0 +1,684 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests + +void test_directory_tests() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_root_directory() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "/")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_creation() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "potato", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_file_creation() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "burito", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_iteration() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "/")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "burito"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "potato"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_failures() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "potato", 0777); + TEST_ASSERT_EQUAL(EEXIST, errno); + res = !((dd[0] = opendir("/fs/" "tomato")) != NULL); + TEST_ASSERT_EQUAL(ENOENT, errno); + res = !((dd[0] = opendir("/fs/" "burito")) != NULL); + TEST_ASSERT_EQUAL(ENOTDIR, errno); + res = !((fd[0] = fopen("/fs/" "tomato", "rb")) != NULL); + TEST_ASSERT_EQUAL(ENOENT, errno); + res = !((fd[0] = fopen("/fs/" "potato", "rb")) != NULL); + TEST_ASSERT_EQUAL(EISDIR, errno); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_nested_directories() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "potato/baked", 0777); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "potato/sweet", 0777); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "potato/fried", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "potato")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "baked"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "fried"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "sweet"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_multi_block_directory() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "cactus", 0777); + TEST_ASSERT_EQUAL(0, res); + for (int i = 0; i < 128; i++) { + sprintf((char *)buffer, "/fs/" "cactus/test%03d", i); + res = mkdir((char *)buffer, 0777); + TEST_ASSERT_EQUAL(0, res); + } + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "cactus")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + for (int i = 0; i < 128; i++) { + sprintf((char *)buffer, "test%03d", i); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + } + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_remove() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = remove("/fs/" "potato"); + TEST_ASSERT_EQUAL(ENOTEMPTY, errno); + res = remove("/fs/" "potato/sweet"); + TEST_ASSERT_EQUAL(0, res); + res = remove("/fs/" "potato/baked"); + TEST_ASSERT_EQUAL(0, res); + res = remove("/fs/" "potato/fried"); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "potato")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = remove("/fs/" "potato"); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "/")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "burito"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "cactus"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "/")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "burito"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "cactus"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_directory_rename() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "coldpotato", 0777); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "coldpotato/baked", 0777); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "coldpotato/sweet", 0777); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "coldpotato/fried", 0777); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = rename("/fs/" "coldpotato", "/fs/" "hotpotato"); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "hotpotato")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "baked"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "fried"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "sweet"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "warmpotato", 0777); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "warmpotato/mushy", 0777); + TEST_ASSERT_EQUAL(0, res); + res = rename("/fs/" "hotpotato", "/fs/" "warmpotato"); + TEST_ASSERT_EQUAL(ENOTEMPTY, errno); + res = remove("/fs/" "warmpotato/mushy"); + TEST_ASSERT_EQUAL(0, res); + res = rename("/fs/" "hotpotato", "/fs/" "warmpotato"); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "warmpotato")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "baked"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "fried"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "sweet"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "coldpotato", 0777); + TEST_ASSERT_EQUAL(0, res); + res = rename("/fs/" "warmpotato/baked", "/fs/" "coldpotato/baked"); + TEST_ASSERT_EQUAL(0, res); + res = rename("/fs/" "warmpotato/sweet", "/fs/" "coldpotato/sweet"); + TEST_ASSERT_EQUAL(0, res); + res = rename("/fs/" "warmpotato/fried", "/fs/" "coldpotato/fried"); + TEST_ASSERT_EQUAL(0, res); + res = remove("/fs/" "coldpotato"); + TEST_ASSERT_EQUAL(ENOTEMPTY, errno); + res = remove("/fs/" "warmpotato"); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "coldpotato")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "baked"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "fried"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "sweet"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("Directory tests", test_directory_tests), + Case("Root directory", test_root_directory), + Case("Directory creation", test_directory_creation), + Case("File creation", test_file_creation), + Case("Directory iteration", test_directory_iteration), + Case("Directory failures", test_directory_failures), + Case("Nested directories", test_nested_directories), + Case("Multi-block directory", test_multi_block_directory), + Case("Directory remove", test_directory_remove), + Case("Directory rename", test_directory_rename), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/files/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/files/main.cpp new file mode 100644 index 00000000000..26ab217d0fa --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/files/main.cpp @@ -0,0 +1,449 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests + +void test_file_tests() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_simple_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "hello", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + size = strlen("Hello World!\n"); + memcpy(wbuffer, "Hello World!\n", size); + res = fwrite(wbuffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "hello", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + size = strlen("Hello World!\n"); + res = fread(rbuffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(rbuffer, wbuffer, size); + TEST_ASSERT_EQUAL(0, res); + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_small_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + size_t size = 32; + size_t chunk = 31; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "smallavacado", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + for (size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + res = fwrite(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 32; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "smallavacado", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = fread(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_medium_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + size_t size = 8192; + size_t chunk = 31; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "mediumavacado", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + for (size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + res = fwrite(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 8192; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "mediumavacado", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = fread(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_large_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + size_t size = 262144; + size_t chunk = 31; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "largeavacado", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + for (size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + res = fwrite(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 262144; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "largeavacado", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = fread(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_non_overlap_check() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + size_t size = 32; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "smallavacado", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = fread(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 8192; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "mediumavacado", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = fread(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + { + size_t size = 262144; + size_t chunk = 29; + srand(0); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "largeavacado", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + for (size_t i = 0; i < size; i += chunk) { + chunk = (chunk < size - i) ? chunk : size - i; + res = fread(buffer, 1, chunk, fd[0]); + TEST_ASSERT_EQUAL(chunk, res); + for (size_t b = 0; b < chunk && i + b < size; b++) { + res = buffer[b]; + TEST_ASSERT_EQUAL(rand() & 0xff, res); + } + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_dir_check() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "/")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "hello"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "largeavacado"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "mediumavacado"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "smallavacado"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("File tests", test_file_tests), + Case("Simple file test", test_simple_file_test), + Case("Small file test", test_small_file_test), + Case("Medium file test", test_medium_file_test), + Case("Large file test", test_large_file_test), + Case("Non-overlap check", test_non_overlap_check), + Case("Dir check", test_dir_check), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/interspersed/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/interspersed/main.cpp new file mode 100644 index 00000000000..01e2eca7197 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/interspersed/main.cpp @@ -0,0 +1,407 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests + +void test_parallel_tests() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_parallel_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "a", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[1] = fopen("/fs/" "b", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[2] = fopen("/fs/" "c", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[3] = fopen("/fs/" "d", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 10; i++) { + res = fwrite((const void *)"a", 1, 1, fd[0]); + TEST_ASSERT_EQUAL(1, res); + res = fwrite((const void *)"b", 1, 1, fd[1]); + TEST_ASSERT_EQUAL(1, res); + res = fwrite((const void *)"c", 1, 1, fd[2]); + TEST_ASSERT_EQUAL(1, res); + res = fwrite((const void *)"d", 1, 1, fd[3]); + TEST_ASSERT_EQUAL(1, res); + } + + fclose(fd[0]); + fclose(fd[1]); + fclose(fd[2]); + fclose(fd[3]); + res = !((dd[0] = opendir("/fs/" "/")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "a"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "b"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "c"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "d"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "a", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[1] = fopen("/fs/" "b", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[2] = fopen("/fs/" "c", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[3] = fopen("/fs/" "d", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 10; i++) { + res = fread(buffer, 1, 1, fd[0]); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('a', res); + res = fread(buffer, 1, 1, fd[1]); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('b', res); + res = fread(buffer, 1, 1, fd[2]); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('c', res); + res = fread(buffer, 1, 1, fd[3]); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('d', res); + } + + fclose(fd[0]); + fclose(fd[1]); + fclose(fd[2]); + fclose(fd[3]); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_parallel_remove_file_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "e", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 5; i++) { + res = fwrite((const void *)"e", 1, 1, fd[0]); + TEST_ASSERT_EQUAL(1, res); + } + res = remove("/fs/" "a"); + TEST_ASSERT_EQUAL(0, res); + res = remove("/fs/" "b"); + TEST_ASSERT_EQUAL(0, res); + res = remove("/fs/" "c"); + TEST_ASSERT_EQUAL(0, res); + res = remove("/fs/" "d"); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 5; i++) { + res = fwrite((const void *)"e", 1, 1, fd[0]); + TEST_ASSERT_EQUAL(1, res); + } + + fclose(fd[0]); + res = !((dd[0] = opendir("/fs/" "/")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "e"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "e", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 10; i++) { + res = fread(buffer, 1, 1, fd[0]); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('e', res); + } + + fclose(fd[0]); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_remove_inconveniently_test() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "e", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[1] = fopen("/fs/" "f", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[2] = fopen("/fs/" "g", "wb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 5; i++) { + res = fwrite((const void *)"e", 1, 1, fd[0]); + TEST_ASSERT_EQUAL(1, res); + res = fwrite((const void *)"f", 1, 1, fd[1]); + TEST_ASSERT_EQUAL(1, res); + res = fwrite((const void *)"g", 1, 1, fd[2]); + TEST_ASSERT_EQUAL(1, res); + } + res = remove("/fs/" "f"); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 5; i++) { + res = fwrite((const void *)"e", 1, 1, fd[0]); + TEST_ASSERT_EQUAL(1, res); + res = fwrite((const void *)"f", 1, 1, fd[1]); + TEST_ASSERT_EQUAL(1, res); + res = fwrite((const void *)"g", 1, 1, fd[2]); + TEST_ASSERT_EQUAL(1, res); + } + + fclose(fd[0]); + fclose(fd[1]); + fclose(fd[2]); + res = !((dd[0] = opendir("/fs/" "/")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_DIR, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "e"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "g"); + TEST_ASSERT_EQUAL(0, res); + res = ed->d_type; + TEST_ASSERT_EQUAL(DT_REG, res); + + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "e", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[1] = fopen("/fs/" "g", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + for (int i = 0; i < 10; i++) { + res = fread(buffer, 1, 1, fd[0]); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('e', res); + res = fread(buffer, 1, 1, fd[1]); + TEST_ASSERT_EQUAL(1, res); + res = buffer[0]; + TEST_ASSERT_EQUAL('g', res); + } + + fclose(fd[0]); + fclose(fd[1]); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("Parallel tests", test_parallel_tests), + Case("Parallel file test", test_parallel_file_test), + Case("Parallel remove file test", test_parallel_remove_file_test), + Case("Remove inconveniently test", test_remove_inconveniently_test), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/seek/main.cpp b/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/seek/main.cpp new file mode 100644 index 00000000000..36a328cda81 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/filesystem_retarget/seek/main.cpp @@ -0,0 +1,641 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#error [NOT_SUPPORTED] Non-volatile block device required +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 480 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests + +void test_seek_tests() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = MBED_TEST_FILESYSTEM::format(&bd); + TEST_ASSERT_EQUAL(0, res); + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = mkdir("/fs/" "hello", 0777); + TEST_ASSERT_EQUAL(0, res); + for (int i = 0; i < 132; i++) { + sprintf((char *)buffer, "/fs/" "hello/kitty%03d", i); + res = !((fd[0] = fopen((char *)buffer, + "ab")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + size = strlen("kittycatcat"); + memcpy(buffer, "kittycatcat", size); + for (int j = 0; j < 132; j++) { + fwrite(buffer, 1, size, fd[0]); + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + } + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_simple_dir_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "hello")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + int i; + for (i = 0; i < 4; i++) { + sprintf((char *)buffer, "kitty%03d", i); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + pos = telldir(dd[0]); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + + seekdir(dd[0], pos); + sprintf((char *)buffer, "kitty%03d", i); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + + rewinddir(dd[0]); + sprintf((char *)buffer, "kitty%03d", 0); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + + seekdir(dd[0], pos); + sprintf((char *)buffer, "kitty%03d", i); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_large_dir_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((dd[0] = opendir("/fs/" "hello")) != NULL); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + int i; + for (i = 0; i < 128; i++) { + sprintf((char *)buffer, "kitty%03d", i); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + pos = telldir(dd[0]); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + + seekdir(dd[0], pos); + sprintf((char *)buffer, "kitty%03d", i); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + + rewinddir(dd[0]); + sprintf((char *)buffer, "kitty%03d", 0); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, "."); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, ".."); + TEST_ASSERT_EQUAL(0, res); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + + seekdir(dd[0], pos); + sprintf((char *)buffer, "kitty%03d", i); + res = ((ed = readdir(dd[0])) != NULL); + TEST_ASSERT_EQUAL(1, res); + res = strcmp(ed->d_name, (char *)buffer); + TEST_ASSERT_EQUAL(0, res); + res = closedir(dd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_simple_file_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "hello/kitty042", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + size = strlen("kittycatcat"); + for (int i = 0; i < 4; i++) { + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + pos = ftell(fd[0]); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + rewind(fd[0]); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], -size, SEEK_CUR); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], -size, SEEK_END); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + res = fseek(fd[0], 0, SEEK_CUR); + TEST_ASSERT_EQUAL(0, res); + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_large_file_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "hello/kitty042", "rb")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + size = strlen("kittycatcat"); + for (int i = 0; i < 128; i++) { + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + pos = ftell(fd[0]); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + rewind(fd[0]); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], -size, SEEK_CUR); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], -size, SEEK_END); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + res = fseek(fd[0], 0, SEEK_CUR); + TEST_ASSERT_EQUAL(0, res); + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_simple_file_seek_and_write() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "hello/kitty042", "r+b")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + size = strlen("kittycatcat"); + for (int i = 0; i < 4; i++) { + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + pos = ftell(fd[0]); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + + memcpy(buffer, "doggodogdog", size); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fwrite(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "doggodogdog", size); + TEST_ASSERT_EQUAL(0, res); + + rewind(fd[0]); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "doggodogdog", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], -size, SEEK_END); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + res = fseek(fd[0], 0, SEEK_CUR); + TEST_ASSERT_EQUAL(0, res); + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_large_file_seek_and_write() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "hello/kitty042", "r+b")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + off_t pos; + size = strlen("kittycatcat"); + for (int i = 0; i < 128; i++) { + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + if (i != 4) { + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + } + pos = ftell(fd[0]); + } + res = pos >= 0; + TEST_ASSERT_EQUAL(1, res); + + memcpy(buffer, "doggodogdog", size); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fwrite(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "doggodogdog", size); + TEST_ASSERT_EQUAL(0, res); + + rewind(fd[0]); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], pos, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "doggodogdog", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], -size, SEEK_END); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + + res = fseek(fd[0], 0, SEEK_CUR); + TEST_ASSERT_EQUAL(0, res); + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_boundary_seek_and_write() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "hello/kitty042", "r+b")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + size = strlen("hedgehoghog"); + const off_t offsets[] = {512, 1020, 513, 1021, 511, 1019}; + + for (int i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) { + off_t off = offsets[i]; + memcpy(buffer, "hedgehoghog", size); + res = fseek(fd[0], off, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fwrite(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = fseek(fd[0], off, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "hedgehoghog", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], 0, SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "kittycatcat", size); + TEST_ASSERT_EQUAL(0, res); + res = fflush(fd[0]); + TEST_ASSERT_EQUAL(0, res); + } + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + +void test_out_of_bounds_seek() +{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); + + { + res = fs.mount(&bd); + TEST_ASSERT_EQUAL(0, res); + res = !((fd[0] = fopen("/fs/" "hello/kitty042", "r+b")) != NULL); + TEST_ASSERT_EQUAL(0, res); + + size = strlen("kittycatcat"); + res = fseek(fd[0], 0, SEEK_END); + TEST_ASSERT_EQUAL(0, res); + res = ftell(fd[0]); + TEST_ASSERT_EQUAL(132 * size, res); + res = fseek(fd[0], (132 + 4) * size, + SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(0, res); + + memcpy(buffer, "porcupineee", size); + res = fwrite(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = fseek(fd[0], (132 + 4) * size, + SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "porcupineee", size); + TEST_ASSERT_EQUAL(0, res); + res = fseek(fd[0], 132 * size, + SEEK_SET); + TEST_ASSERT_EQUAL(0, res); + res = fread(buffer, 1, size, fd[0]); + TEST_ASSERT_EQUAL(size, res); + res = memcmp(buffer, "\0\0\0\0\0\0\0\0\0\0\0", size); + TEST_ASSERT_EQUAL(0, res); + res = fclose(fd[0]); + TEST_ASSERT_EQUAL(0, res); + res = fs.unmount(); + TEST_ASSERT_EQUAL(0, res); + } + + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +} + + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +} + +Case cases[] = { + Case("Seek tests", test_seek_tests), + Case("Simple dir seek", test_simple_dir_seek), + Case("Large dir seek", test_large_dir_seek), + Case("Simple file seek", test_simple_file_seek), + Case("Large file seek", test_large_file_seek), + Case("Simple file seek and write", test_simple_file_seek_and_write), + Case("Large file seek and write", test_large_file_seek_and_write), + Case("Boundary seek and write", test_boundary_seek_and_write), + Case("Out-of-bounds seek", test_out_of_bounds_seek), +}; + +Specification specification(test_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/features/storage/filesystem/littlefsv2/TESTS/host_tests/unexpected_reset.py b/features/storage/filesystem/littlefsv2/TESTS/host_tests/unexpected_reset.py new file mode 100644 index 00000000000..e85f3d10f01 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/host_tests/unexpected_reset.py @@ -0,0 +1,103 @@ +""" +mbed SDK +Copyright (c) 2017-2017 ARM Limited + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from __future__ import print_function + +from mbed_host_tests import BaseHostTest +from time import sleep + + +class UnexpectedResetTest(BaseHostTest): + """This test checks that a device's RTC keeps count through a reset + + It does this by setting the RTC's time, triggering a reset, + delaying and then reading the RTC's time again to ensure + that the RTC is still counting. + """ + + """Number of times to reset the device in this test""" + RESET_COUNT = 20 + RESET_DELAY_BASE = 1.0 + RESET_DELAY_INC = 0.02 + VALUE_PLACEHOLDER = "0" + + def setup(self): + """Register callbacks required for the test""" + self._error = False + generator = self.unexpected_reset_test() + generator.next() + + def run_gen(key, value, time): + """Run the generator, and fail testing if the iterator stops""" + if self._error: + return + try: + generator.send((key, value, time)) + except StopIteration: + self._error = True + + for resp in ("start", "read", "format_done", "reset_complete"): + self.register_callback(resp, run_gen) + + def teardown(self): + """No work to do here""" + pass + + def unexpected_reset_test(self): + """Generator for running the reset test + + This function calls yield to wait for the next event from + the device. If the device gives the wrong response, then the + generator terminates by returing which raises a StopIteration + exception and fails the test. + """ + + # Wait for start token + key, value, time = yield + if key != "start": + return + + # Format the device before starting the test + self.send_kv("format", self.VALUE_PLACEHOLDER) + key, value, time = yield + if key != "format_done": + return + + for i in range(self.RESET_COUNT): + + self.send_kv("run", self.VALUE_PLACEHOLDER) + sleep(self.RESET_DELAY_BASE + self.RESET_DELAY_INC * i) + + self.reset() + + # Wait for start token + key, value, time = yield + self.log("Key from yield: %s" % key) + if key != "reset_complete": + return + + + self.send_kv("__sync", "00000000-0000-000000000-000000000000") + + # Wait for start token + key, value, time = yield + if key != "start": + return + + self.send_kv("exit", "pass") + + yield # No more events expected + diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/Makefile b/features/storage/filesystem/littlefsv2/TESTS/util/Makefile new file mode 100644 index 00000000000..78a0bec49df --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/Makefile @@ -0,0 +1,21 @@ + +all: test_dirs test_files test_seek test_parallel + +test_%: ../../littlefs/tests/test_%.sh + cp $< $(notdir $<) + sed -i -e 's/tests\//.\//' -e 's/echo/.\/echo.py/' $(notdir $<) + + ./clean.sh + ln -f -s replacements_mbed.yml replacements.yml + ./$(notdir $<) + mkdir -p ../filesystem/$(patsubst test_%,%,$@) + cp main.cpp ../filesystem/$(patsubst test_%,%,$@)/main.cpp + + ./clean.sh + ln -f -s replacements_retarget.yml replacements.yml + ./$(notdir $<) + mkdir -p ../filesystem_retarget/$(patsubst test_%,%,$@) + cp main.cpp ../filesystem_retarget/$(patsubst test_%,%,$@)/main.cpp + +clean: + ./clean.sh diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/clean.sh b/features/storage/filesystem/littlefsv2/TESTS/util/clean.sh new file mode 100755 index 00000000000..ba9862db28a --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/clean.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +rm -f main.cpp +rm -f template_all_names.txt diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/echo.py b/features/storage/filesystem/littlefsv2/TESTS/util/echo.py new file mode 100755 index 00000000000..9d6a1c1881c --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/echo.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +import re +import sys +import subprocess +import os + +def main(*args): + desc = ' '.join(args).strip('-= ') + name = 'test_' + desc.lower().replace(' ', '_').replace('-', '_') + + exists = os.path.isfile('template_all_names.txt') + + with open('template_all_names.txt', 'a') as file: + file.write(name + '\n') + file.write(desc + '\n') + + with open('template_unit.fmt') as file: + template = file.read() + + template_header, template_footer = template.split('{test}') + + if exists: + with open('main.cpp', 'a') as file: + file.write(template_footer.format( + test_name=name)) + + if name != 'test_results': + with open('main.cpp', 'a') as file: + file.write(template_header.format( + test_name=name)) + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/replacements_mbed.yml b/features/storage/filesystem/littlefsv2/TESTS/util/replacements_mbed.yml new file mode 100644 index 00000000000..676a3d38c87 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/replacements_mbed.yml @@ -0,0 +1,33 @@ +- ['lfs2_format\(&lfs2, &cfg\)', 'MBED_TEST_FILESYSTEM::format(&bd)'] +- ['lfs2_mount\(&lfs2, &cfg\)', 'fs.mount(&bd)'] +- ['lfs2_unmount\(&lfs2\)', 'fs.unmount()'] +- ['lfs2_mkdir\(&lfs2, (.*)\)', 'fs.mkdir(\1, 0777)'] +- ['lfs2_remove\(&lfs2, (.*)\)', 'fs.remove(\1)'] +- ['lfs2_rename\(&lfs2, (.*), ?(.*)\)', 'fs.rename(\1, \2)'] + +- ['lfs2_dir_open\(&lfs2, &dir\[(.*)\], ?(.*)\)', 'dir[\1].open(&fs, \2)'] +- ['lfs2_dir_close\(&lfs2, &dir\[(.*)\]\)', 'dir[\1].close()'] +- ['lfs2_dir_read\(&lfs2, &dir\[(.*)\], &info\)', 'dir[\1].read(&ent)'] +- ['lfs2_dir_seek\(&lfs2, &dir\[(.*)\], ?(.*)\).*;', 'dir[\1].seek(\2);'] # no dir errors +- ['lfs2_dir_rewind\(&lfs2, &dir\[(.*)\]\).*;', 'dir[\1].rewind();'] # no dir errors +- ['lfs2_dir_tell\(&lfs2, &dir\[(.*)\]\)', 'dir[\1].tell()'] + +- ['lfs2_file_open\(&lfs2, &file\[(.*)\], ?(.*)\)', 'file[\1].open(&fs, \2)'] +- ['lfs2_file_close\(&lfs2, &file\[(.*)\]\)', 'file[\1].close()'] +- ['lfs2_file_sync\(&lfs2, &file\[(.*)\]\)', 'file[\1].sync()'] +- ['lfs2_file_write\(&lfs2, &file\[(.*)\], ?(.*), (.*)\)', 'file[\1].write(\2, \3)'] +- ['lfs2_file_read\(&lfs2, &file\[(.*)\], ?(.*), (.*)\)', 'file[\1].read(\2, \3)'] +- ['lfs2_file_seek\(&lfs2, &file\[(.*)\], ?(.*)\)', 'file[\1].seek(\2)'] +- ['lfs2_file_tell\(&lfs2, &file\[(.*)\]\)', 'file[\1].tell()'] +- ['lfs2_file_rewind\(&lfs2, &file\[(.*)\]\).*;', 'file[\1].rewind();'] # no errors +- ['lfs2_file_size\(&lfs2, &file\[(.*)\]\)', 'file[\1].size()'] + +- ['LFS2_TYPE_([A-Z]+)', 'DT_\1'] +- ['LFS2_O_([A-Z]+)', 'O_\1'] +- ['LFS2_SEEK_([A-Z]+)', 'SEEK_\1'] +- ['LFS2_ERR_([A-Z]+)', '-E\1'] +- ['lfs2_(s?)size_t', '\1size_t'] +- ['lfs2_soff_t', 'off_t'] +- ['info\.name', 'ent.d_name'] +- ['info\.type', 'ent.d_type'] +- ['^.*info\.size.*$', ''] # dirent sizes not supported diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/replacements_retarget.yml b/features/storage/filesystem/littlefsv2/TESTS/util/replacements_retarget.yml new file mode 100644 index 00000000000..e2ae650c45a --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/replacements_retarget.yml @@ -0,0 +1,37 @@ +- ['lfs2_format\(&lfs2, &cfg\)', 'MBED_TEST_FILESYSTEM::format(&bd)'] +- ['lfs2_mount\(&lfs2, &cfg\)', 'fs.mount(&bd)'] +- ['lfs2_unmount\(&lfs2\)', 'fs.unmount()'] +- ['lfs2_mkdir\(&lfs2, (.*)\)', 'mkdir("/fs/" \1, 0777)'] +- ['lfs2_remove\(&lfs2, (.*)\)', 'remove("/fs/" \1)'] +- ['lfs2_rename\(&lfs2, (.*), ?(.*)\)', 'rename("/fs/" \1, "/fs/" \2)'] + +- ['lfs2_dir_open\(&lfs2, &dir\[(.*)\], ?(.*)\)', '!((dd[\1] = opendir("/fs/" \2)) != NULL)'] +- ['lfs2_dir_close\(&lfs2, &dir\[(.*)\]\)', 'closedir(dd[\1])'] +- ['lfs2_dir_read\(&lfs2, &dir\[(.*)\], &info\)', '((ed = readdir(dd[\1])) != NULL)'] +- ['lfs2_dir_seek\(&lfs2, &dir\[(.*)\], ?(.*)\).*;', 'seekdir(dd[\1], \2);'] # no dir errors +- ['lfs2_dir_rewind\(&lfs2, &dir\[(.*)\]\).*;', 'rewinddir(dd[\1]);'] # no dir errors +- ['lfs2_dir_tell\(&lfs2, &dir\[(.*)\]\)', 'telldir(dd[\1])'] + +- ['lfs2_file_open\(&lfs2, &file\[(.*)\], ?(.*)\)', '!((fd[\1] = fopen("/fs/" \2)) != NULL)'] +- ['lfs2_file_close\(&lfs2, &file\[(.*)\]\)', 'fclose(fd[\1])'] +- ['lfs2_file_sync\(&lfs2, &file\[(.*)\]\)', 'fflush(fd[\1])'] +- ['lfs2_file_write\(&lfs2, &file\[(.*)\], ?(.*), (.*)\)', 'fwrite(\2, 1, \3, fd[\1])'] +- ['lfs2_file_read\(&lfs2, &file\[(.*)\], ?(.*), (.*)\)', 'fread(\2, 1, \3, fd[\1])'] +- ['lfs2_file_tell\(&lfs2, &file\[(.*)\]\)', 'ftell(fd[\1])'] +- ['lfs2_file_rewind\(&lfs2, &file\[(.*)\]\).*;', 'rewind(fd[\1]);'] # no errors + +- ['LFS2_TYPE_([A-Z]+)', 'DT_\1'] +- ['LFS2_SEEK_([A-Z]+)', 'SEEK_\1'] +- ['LFS2_ERR_([A-Z]+)', '-E\1'] +- ['lfs2_(s?)size_t', '\1size_t'] +- ['lfs2_soff_t', 'off_t'] +- ['info\.name', 'ed->d_name'] +- ['info\.type', 'ed->d_type'] +- ['^.*info\.size.*$', ''] # dirent sizes not supported + +- ['LFS2_O_WRONLY \| LFS2_O_CREAT \| LFS2_O_APPEND', '"ab"'] +- ['LFS2_O_WRONLY \| LFS2_O_TRUNC', '"wb"'] +- ['LFS2_O_CREAT \| LFS2_O_WRONLY', '"wb"'] +- ['LFS2_O_WRONLY \| LFS2_O_CREAT', '"wb"'] +- ['LFS2_O_RDONLY', '"rb"'] +- ['LFS2_O_RDWR', '"r+b"'] diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/stats.py b/features/storage/filesystem/littlefsv2/TESTS/util/stats.py new file mode 100755 index 00000000000..6bd1b98fe2f --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/stats.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +import re +import sys +import subprocess +import os + +def main(*args): + with open('main.cpp') as file: + tests = file.read() + + cases = [] + with open('template_all_names.txt') as file: + while True: + name = file.readline().strip('\n') + desc = file.readline().strip('\n') + if name == 'test_results': + break + + cases.append((name, desc)) + + with open('template_wrapper.fmt') as file: + template = file.read() + + with open('main.cpp', 'w') as file: + file.write(template.format( + tests=tests, + test_cases='\n'.join( + 4*' '+'Case("{desc}", {name}),'.format( + name=name, desc=desc) for name, desc in cases))) + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/template_subunit.fmt b/features/storage/filesystem/littlefsv2/TESTS/util/template_subunit.fmt new file mode 100644 index 00000000000..cdcaab9ff76 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/template_subunit.fmt @@ -0,0 +1,4 @@ + + {{ +{test} + }} diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/template_unit.fmt b/features/storage/filesystem/littlefsv2/TESTS/util/template_unit.fmt new file mode 100644 index 00000000000..06c7f1157ed --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/template_unit.fmt @@ -0,0 +1,8 @@ + +void {test_name}() {{ + int res = bd.init(); + TEST_ASSERT_EQUAL(0, res); +{test} + res = bd.deinit(); + TEST_ASSERT_EQUAL(0, res); +}} diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/template_wrapper.fmt b/features/storage/filesystem/littlefsv2/TESTS/util/template_wrapper.fmt new file mode 100644 index 00000000000..9a4999c7316 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/template_wrapper.fmt @@ -0,0 +1,86 @@ +#include "mbed.h" +#include "greentea-client/test_env.h" +#include "unity.h" +#include "utest.h" +#include +#include + +using namespace utest::v1; + +// test configuration +#ifndef MBED_TEST_FILESYSTEM +#define MBED_TEST_FILESYSTEM LittleFileSystem2 +#endif + +#ifndef MBED_TEST_FILESYSTEM_DECL +#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs") +#endif + +#ifndef MBED_TEST_BLOCKDEVICE +#define MBED_TEST_BLOCKDEVICE SPIFBlockDevice +#define MBED_TEST_BLOCKDEVICE_DECL SPIFBlockDevice bd(PTE2, PTE4, PTE1, PTE5) +#endif + +#ifndef MBED_TEST_BLOCKDEVICE_DECL +#define MBED_TEST_BLOCKDEVICE_DECL MBED_TEST_BLOCKDEVICE bd +#endif + +#ifndef MBED_TEST_FILES +#define MBED_TEST_FILES 4 +#endif + +#ifndef MBED_TEST_DIRS +#define MBED_TEST_DIRS 4 +#endif + +#ifndef MBED_TEST_BUFFER +#define MBED_TEST_BUFFER 8192 +#endif + +#ifndef MBED_TEST_TIMEOUT +#define MBED_TEST_TIMEOUT 120 +#endif + + +// declarations +#define STRINGIZE(x) STRINGIZE2(x) +#define STRINGIZE2(x) #x +#define INCLUDE(x) STRINGIZE(x.h) + +#include INCLUDE(MBED_TEST_FILESYSTEM) +#include INCLUDE(MBED_TEST_BLOCKDEVICE) + +MBED_TEST_FILESYSTEM_DECL; +MBED_TEST_BLOCKDEVICE_DECL; + +Dir dir[MBED_TEST_DIRS]; +File file[MBED_TEST_FILES]; +DIR *dd[MBED_TEST_DIRS]; +FILE *fd[MBED_TEST_FILES]; +struct dirent ent; +struct dirent *ed; +size_t size; +uint8_t buffer[MBED_TEST_BUFFER]; +uint8_t rbuffer[MBED_TEST_BUFFER]; +uint8_t wbuffer[MBED_TEST_BUFFER]; + + +// tests +{tests} + + +// test setup +utest::v1::status_t test_setup(const size_t number_of_cases) {{ + GREENTEA_SETUP(MBED_TEST_TIMEOUT, "default_auto"); + return verbose_test_setup_handler(number_of_cases); +}} + +Case cases[] = {{ +{test_cases} +}}; + +Specification specification(test_setup, cases); + +int main() {{ + return !Harness::run(specification); +}} diff --git a/features/storage/filesystem/littlefsv2/TESTS/util/test.py b/features/storage/filesystem/littlefsv2/TESTS/util/test.py new file mode 100755 index 00000000000..fc98dd289e4 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/TESTS/util/test.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +import re +import sys +import subprocess +import os +import yaml + +def generate(test): + with open('replacements.yml') as file: + replacements = yaml.load(file) + + lines = [] + for line in re.split('(?<=[;{}])\n', test.read()): + for pattern, replacement in replacements: + line = re.sub(pattern, replacement, line, 0, re.DOTALL | re.MULTILINE) + + match = re.match('(?: *\n)*( *)(.*)=>(.*);', line, re.DOTALL | re.MULTILINE) + if match: + tab, test, expect = match.groups() + lines.append(tab+'res = {test};'.format(test=test.strip())) + lines.append(tab+'TEST_ASSERT_EQUAL({expect}, res);'.format( + name=re.match('\w*', test.strip()).group(), + expect=expect.strip())) + else: + lines.append(line) + + lines = lines[:-1] + + with open('template_subunit.fmt') as file: + template = file.read() + + with open('main.cpp', 'a') as file: + file.write(template.format( + test=('\n'.join( + 4*' '+line.replace('\n', '\n'+4*' ') + for line in lines)))) + +def main(test=None): + if test and not test.startswith('-'): + with open(test) as file: + generate(file) + else: + generate(sys.stdin) + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/features/storage/filesystem/littlefsv2/littlefs/.gitignore b/features/storage/filesystem/littlefsv2/littlefs/.gitignore new file mode 100644 index 00000000000..3e6200e2c49 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/.gitignore @@ -0,0 +1,12 @@ +# Compilation output +*.o +*.d +*.a + +# Testing things +blocks/ +lfs2 +test.c +tests/*.toml.* +scripts/__pycache__ +.gdb_history diff --git a/features/storage/filesystem/littlefsv2/littlefs/.travis.yml b/features/storage/filesystem/littlefsv2/littlefs/.travis.yml new file mode 100644 index 00000000000..ecee6a8712d --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/.travis.yml @@ -0,0 +1,429 @@ +# environment variables +env: + global: + - CFLAGS=-Werror + - MAKEFLAGS=-j + +# cache installation dirs +cache: + pip: true + directories: + - $HOME/.cache/apt + +# common installation +_: &install-common + # need toml, also pip3 isn't installed by default? + - sudo apt-get install python3 python3-pip + - sudo pip3 install toml + # setup a ram-backed disk to speed up reentrant tests + - mkdir disks + - sudo mount -t tmpfs -o size=100m tmpfs disks + - export TFLAGS="$TFLAGS --disk=disks/disk" + +# test cases +_: &test-example + # make sure example can at least compile + - sed -n '/``` c/,/```/{/```/d; p}' README.md > test.c && + make all CFLAGS+=" + -Duser_provided_block_device_read=NULL + -Duser_provided_block_device_prog=NULL + -Duser_provided_block_device_erase=NULL + -Duser_provided_block_device_sync=NULL + -include stdio.h" +# default tests +_: &test-default + # normal+reentrant tests + - make test TFLAGS+="-nrk" +# common real-life geometries +_: &test-nor + # NOR flash: read/prog = 1 block = 4KiB + - make test TFLAGS+="-nrk -DLFS2_READ_SIZE=1 -DLFS2_BLOCK_SIZE=4096" +_: &test-emmc + # eMMC: read/prog = 512 block = 512 + - make test TFLAGS+="-nrk -DLFS2_READ_SIZE=512 -DLFS2_BLOCK_SIZE=512" +_: &test-nand + # NAND flash: read/prog = 4KiB block = 32KiB + - make test TFLAGS+="-nrk -DLFS2_READ_SIZE=4096 -DLFS2_BLOCK_SIZE=\(32*1024\)" +# other extreme geometries that are useful for testing various corner cases +_: &test-no-intrinsics + - make test TFLAGS+="-nrk -DLFS2_NO_INTRINSICS" +_: &test-no-inline + - make test TFLAGS+="-nrk -DLFS2_INLINE_MAX=0" +_: &test-byte-writes + - make test TFLAGS+="-nrk -DLFS2_READ_SIZE=1 -DLFS2_CACHE_SIZE=1" +_: &test-block-cycles + - make test TFLAGS+="-nrk -DLFS2_BLOCK_CYCLES=1" +_: &test-odd-block-count + - make test TFLAGS+="-nrk -DLFS2_BLOCK_COUNT=1023 -DLFS2_LOOKAHEAD_SIZE=256" +_: &test-odd-block-size + - make test TFLAGS+="-nrk -DLFS2_READ_SIZE=11 -DLFS2_BLOCK_SIZE=704" + +# report size +_: &report-size + # compile and find the code size with the smallest configuration + - make -j1 clean size + OBJ="$(ls lfs2*.c | sed 's/\.c/\.o/' | tr '\n' ' ')" + CFLAGS+="-DLFS2_NO_ASSERT -DLFS2_NO_DEBUG -DLFS2_NO_WARN -DLFS2_NO_ERROR" + | tee sizes + # update status if we succeeded, compare with master if possible + - | + if [ "$TRAVIS_TEST_RESULT" -eq 0 ] + then + CURR=$(tail -n1 sizes | awk '{print $1}') + PREV=$(curl -u "$GEKY_BOT_STATUSES" https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/master \ + | jq -re "select(.sha != \"$TRAVIS_COMMIT\") + | .statuses[] | select(.context == \"${TRAVIS_BUILD_STAGE_NAME,,}/$NAME\").description + | capture(\"code size is (?[0-9]+)\").size" \ + || echo 0) + + STATUS="Passed, code size is ${CURR}B" + if [ "$PREV" -ne 0 ] + then + STATUS="$STATUS ($(python -c "print '%+.2f' % (100*($CURR-$PREV)/$PREV.0)")%)" + fi + fi + +# stage control +stages: + - name: test + - name: deploy + if: branch = master AND type = push + +# job control +jobs: + # native testing + - &x86 + stage: test + env: + - NAME=littlefs-x86 + install: *install-common + script: [*test-example, *report-size] + - {<<: *x86, script: [*test-default, *report-size]} + - {<<: *x86, script: [*test-nor, *report-size]} + - {<<: *x86, script: [*test-emmc, *report-size]} + - {<<: *x86, script: [*test-nand, *report-size]} + - {<<: *x86, script: [*test-no-intrinsics, *report-size]} + - {<<: *x86, script: [*test-no-inline, *report-size]} + - {<<: *x86, script: [*test-byte-writes, *report-size]} + - {<<: *x86, script: [*test-block-cycles, *report-size]} + - {<<: *x86, script: [*test-odd-block-count, *report-size]} + - {<<: *x86, script: [*test-odd-block-size, *report-size]} + + # cross-compile with ARM (thumb mode) + - &arm + stage: test + env: + - NAME=littlefs-arm + - CC="arm-linux-gnueabi-gcc --static -mthumb" + - TFLAGS="$TFLAGS --exec=qemu-arm" + install: + - *install-common + - sudo apt-get install + gcc-arm-linux-gnueabi + libc6-dev-armel-cross + qemu-user + - arm-linux-gnueabi-gcc --version + - qemu-arm -version + script: [*test-example, *report-size] + - {<<: *arm, script: [*test-default, *report-size]} + - {<<: *arm, script: [*test-nor, *report-size]} + - {<<: *arm, script: [*test-emmc, *report-size]} + - {<<: *arm, script: [*test-nand, *report-size]} + - {<<: *arm, script: [*test-no-intrinsics, *report-size]} + - {<<: *arm, script: [*test-no-inline, *report-size]} + # it just takes way to long to run byte-level writes in qemu, + # note this is still tested in the native tests + #- {<<: *arm, script: [*test-byte-writes, *report-size]} + - {<<: *arm, script: [*test-block-cycles, *report-size]} + - {<<: *arm, script: [*test-odd-block-count, *report-size]} + - {<<: *arm, script: [*test-odd-block-size, *report-size]} + + # cross-compile with MIPS + - &mips + stage: test + env: + - NAME=littlefs-mips + - CC="mips-linux-gnu-gcc --static" + - TFLAGS="$TFLAGS --exec=qemu-mips" + install: + - *install-common + - sudo apt-get install + gcc-mips-linux-gnu + libc6-dev-mips-cross + qemu-user + - mips-linux-gnu-gcc --version + - qemu-mips -version + script: [*test-example, *report-size] + - {<<: *mips, script: [*test-default, *report-size]} + - {<<: *mips, script: [*test-nor, *report-size]} + - {<<: *mips, script: [*test-emmc, *report-size]} + - {<<: *mips, script: [*test-nand, *report-size]} + - {<<: *mips, script: [*test-no-intrinsics, *report-size]} + - {<<: *mips, script: [*test-no-inline, *report-size]} + # it just takes way to long to run byte-level writes in qemu, + # note this is still tested in the native tests + #- {<<: *mips, script: [*test-byte-writes, *report-size]} + - {<<: *mips, script: [*test-block-cycles, *report-size]} + - {<<: *mips, script: [*test-odd-block-count, *report-size]} + - {<<: *mips, script: [*test-odd-block-size, *report-size]} + + # cross-compile with PowerPC + - &powerpc + stage: test + env: + - NAME=littlefs-powerpc + - CC="powerpc-linux-gnu-gcc --static" + - TFLAGS="$TFLAGS --exec=qemu-ppc" + install: + - *install-common + - sudo apt-get install + gcc-powerpc-linux-gnu + libc6-dev-powerpc-cross + qemu-user + - powerpc-linux-gnu-gcc --version + - qemu-ppc -version + script: [*test-example, *report-size] + - {<<: *powerpc, script: [*test-default, *report-size]} + - {<<: *powerpc, script: [*test-nor, *report-size]} + - {<<: *powerpc, script: [*test-emmc, *report-size]} + - {<<: *powerpc, script: [*test-nand, *report-size]} + - {<<: *powerpc, script: [*test-no-intrinsics, *report-size]} + - {<<: *powerpc, script: [*test-no-inline, *report-size]} + # it just takes way to long to run byte-level writes in qemu, + # note this is still tested in the native tests + #- {<<: *powerpc, script: [*test-byte-writes, *report-size]} + - {<<: *powerpc, script: [*test-block-cycles, *report-size]} + - {<<: *powerpc, script: [*test-odd-block-count, *report-size]} + - {<<: *powerpc, script: [*test-odd-block-size, *report-size]} + + # test under valgrind, checking for memory errors + - &valgrind + stage: test + env: + - NAME=littlefs-valgrind + install: + - *install-common + - sudo apt-get install valgrind + - valgrind --version + script: + - make test TFLAGS+="-k --valgrind" + + # self-host with littlefs-fuse for fuzz test + - stage: test + env: + - NAME=littlefs-fuse + if: branch !~ -prefix$ + install: + - *install-common + - sudo apt-get install libfuse-dev + - git clone --depth 1 https://github.com/geky/littlefs-fuse -b v2 + - fusermount -V + - gcc --version + + # setup disk for littlefs-fuse + - rm -rf littlefs-fuse/littlefs/* + - cp -r $(git ls-tree --name-only HEAD) littlefs-fuse/littlefs + + - mkdir mount + - sudo chmod a+rw /dev/loop0 + - dd if=/dev/zero bs=512 count=128K of=disk + - losetup /dev/loop0 disk + script: + # self-host test + - make -C littlefs-fuse + + - littlefs-fuse/lfs2 --format /dev/loop0 + - littlefs-fuse/lfs2 /dev/loop0 mount + + - ls mount + - mkdir mount/littlefs + - cp -r $(git ls-tree --name-only HEAD) mount/littlefs + - cd mount/littlefs + - stat . + - ls -flh + - make -B test + + # test migration using littlefs-fuse + - stage: test + env: + - NAME=littlefs-migration + if: branch !~ -prefix$ + install: + - *install-common + - sudo apt-get install libfuse-dev + - git clone --depth 1 https://github.com/geky/littlefs-fuse -b v2 v2 + - git clone --depth 1 https://github.com/geky/littlefs-fuse -b v1 v1 + - fusermount -V + - gcc --version + + # setup disk for littlefs-fuse + - rm -rf v2/littlefs/* + - cp -r $(git ls-tree --name-only HEAD) v2/littlefs + + - mkdir mount + - sudo chmod a+rw /dev/loop0 + - dd if=/dev/zero bs=512 count=128K of=disk + - losetup /dev/loop0 disk + script: + # compile v1 and v2 + - make -C v1 + - make -C v2 + + # run self-host test with v1 + - v1/lfs2 --format /dev/loop0 + - v1/lfs2 /dev/loop0 mount + + - ls mount + - mkdir mount/littlefs + - cp -r $(git ls-tree --name-only HEAD) mount/littlefs + - cd mount/littlefs + - stat . + - ls -flh + - make -B test + + # attempt to migrate + - cd ../.. + - fusermount -u mount + + - v2/lfs2 --migrate /dev/loop0 + - v2/lfs2 /dev/loop0 mount + + # run self-host test with v2 right where we left off + - ls mount + - cd mount/littlefs + - stat . + - ls -flh + - make -B test + + # automatically create releases + - stage: deploy + env: + - NAME=deploy + script: + - | + bash << 'SCRIPT' + set -ev + # Find version defined in lfs2.h + LFS2_VERSION=$(grep -ox '#define LFS2_VERSION .*' lfs2.h | cut -d ' ' -f3) + LFS2_VERSION_MAJOR=$((0xffff & ($LFS2_VERSION >> 16))) + LFS2_VERSION_MINOR=$((0xffff & ($LFS2_VERSION >> 0))) + # Grab latests patch from repo tags, default to 0, needs finagling + # to get past github's pagination api + PREV_URL=https://api.github.com/repos/$TRAVIS_REPO_SLUG/git/refs/tags/v$LFS2_VERSION_MAJOR.$LFS2_VERSION_MINOR. + PREV_URL=$(curl -u "$GEKY_BOT_RELEASES" "$PREV_URL" -I \ + | sed -n '/^Link/{s/.*<\(.*\)>; rel="last"/\1/;p;q0};$q1' \ + || echo $PREV_URL) + LFS2_VERSION_PATCH=$(curl -u "$GEKY_BOT_RELEASES" "$PREV_URL" \ + | jq 'map(.ref | match("\\bv.*\\..*\\.(.*)$";"g") + .captures[].string | tonumber) | max + 1' \ + || echo 0) + # We have our new version + LFS2_VERSION="v$LFS2_VERSION_MAJOR.$LFS2_VERSION_MINOR.$LFS2_VERSION_PATCH" + echo "VERSION $LFS2_VERSION" + # Check that we're the most recent commit + CURRENT_COMMIT=$(curl -f -u "$GEKY_BOT_RELEASES" \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/commits/master \ + | jq -re '.sha') + [ "$TRAVIS_COMMIT" == "$CURRENT_COMMIT" ] || exit 0 + # Create major branch + git branch v$LFS2_VERSION_MAJOR HEAD + # Create major prefix branch + git config user.name "geky bot" + git config user.email "bot@geky.net" + git fetch https://github.com/$TRAVIS_REPO_SLUG.git \ + --depth=50 v$LFS2_VERSION_MAJOR-prefix || true + ./scripts/prefix.py lfs2$LFS2_VERSION_MAJOR + git branch v$LFS2_VERSION_MAJOR-prefix $( \ + git commit-tree $(git write-tree) \ + $(git rev-parse --verify -q FETCH_HEAD | sed -e 's/^/-p /') \ + -p HEAD \ + -m "Generated v$LFS2_VERSION_MAJOR prefixes") + git reset --hard + # Update major version branches (vN and vN-prefix) + git push --atomic https://$GEKY_BOT_RELEASES@github.com/$TRAVIS_REPO_SLUG.git \ + v$LFS2_VERSION_MAJOR \ + v$LFS2_VERSION_MAJOR-prefix + # Build release notes + PREV=$(git tag --sort=-v:refname -l "v*" | head -1) + if [ ! -z "$PREV" ] + then + echo "PREV $PREV" + CHANGES=$(git log --oneline $PREV.. --grep='^Merge' --invert-grep) + printf "CHANGES\n%s\n\n" "$CHANGES" + fi + case ${GEKY_BOT_DRAFT:-minor} in + true) DRAFT=true ;; + minor) DRAFT=$(jq -R 'endswith(".0")' <<< "$LFS2_VERSION") ;; + false) DRAFT=false ;; + esac + # Create the release and patch version tag (vN.N.N) + curl -f -u "$GEKY_BOT_RELEASES" -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases \ + -d "{ + \"tag_name\": \"$LFS2_VERSION\", + \"name\": \"${LFS2_VERSION%.0}\", + \"target_commitish\": \"$TRAVIS_COMMIT\", + \"draft\": $DRAFT, + \"body\": $(jq -sR '.' <<< "$CHANGES") + }" #" + SCRIPT + +# manage statuses +before_install: + - | + # don't clobber other (not us) failures + if ! curl https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + | jq -e ".statuses[] | select( + .context == \"${TRAVIS_BUILD_STAGE_NAME,,}/$NAME\" and + .state == \"failure\" and + (.target_url | endswith(\"$TRAVIS_JOB_NUMBER\") | not))" + then + curl -u "$GEKY_BOT_STATUSES" -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + -d "{ + \"context\": \"${TRAVIS_BUILD_STAGE_NAME,,}/$NAME\", + \"state\": \"pending\", + \"description\": \"${STATUS:-In progress}\", + \"target_url\": \"$TRAVIS_JOB_WEB_URL#$TRAVIS_JOB_NUMBER\" + }" + fi + +after_failure: + - | + # don't clobber other (not us) failures + if ! curl https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + | jq -e ".statuses[] | select( + .context == \"${TRAVIS_BUILD_STAGE_NAME,,}/$NAME\" and + .state == \"failure\" and + (.target_url | endswith(\"$TRAVIS_JOB_NUMBER\") | not))" + then + curl -u "$GEKY_BOT_STATUSES" -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + -d "{ + \"context\": \"${TRAVIS_BUILD_STAGE_NAME,,}/$NAME\", + \"state\": \"failure\", + \"description\": \"${STATUS:-Failed}\", + \"target_url\": \"$TRAVIS_JOB_WEB_URL#$TRAVIS_JOB_NUMBER\" + }" + fi + +after_success: + - | + # don't clobber other (not us) failures + # only update if we were last job to mark in progress, + # this isn't perfect but is probably good enough + if ! curl https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + | jq -e ".statuses[] | select( + .context == \"${TRAVIS_BUILD_STAGE_NAME,,}/$NAME\" and + (.state == \"failure\" or .state == \"pending\") and + (.target_url | endswith(\"$TRAVIS_JOB_NUMBER\") | not))" + then + curl -u "$GEKY_BOT_STATUSES" -X POST \ + https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ + -d "{ + \"context\": \"${TRAVIS_BUILD_STAGE_NAME,,}/$NAME\", + \"state\": \"success\", + \"description\": \"${STATUS:-Passed}\", + \"target_url\": \"$TRAVIS_JOB_WEB_URL#$TRAVIS_JOB_NUMBER\" + }" + fi diff --git a/features/storage/filesystem/littlefsv2/littlefs/DESIGN.md b/features/storage/filesystem/littlefsv2/littlefs/DESIGN.md new file mode 100644 index 00000000000..1d02ba3bfcc --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/DESIGN.md @@ -0,0 +1,2173 @@ +## The design of littlefs + +A little fail-safe filesystem designed for microcontrollers. + +``` + | | | .---._____ + .-----. | | +--|o |---| littlefs | +--| |---| | + '-----' '----------' + | | | +``` + +littlefs was originally built as an experiment to learn about filesystem design +in the context of microcontrollers. The question was: How would you build a +filesystem that is resilient to power-loss and flash wear without using +unbounded memory? + +This document covers the high-level design of littlefs, how it is different +than other filesystems, and the design decisions that got us here. For the +low-level details covering every bit on disk, check out [SPEC.md](SPEC.md). + +## The problem + +The embedded systems littlefs targets are usually 32-bit microcontrollers with +around 32 KiB of RAM and 512 KiB of ROM. These are often paired with SPI NOR +flash chips with about 4 MiB of flash storage. These devices are too small for +Linux and most existing filesystems, requiring code written specifically with +size in mind. + +Flash itself is an interesting piece of technology with its own quirks and +nuance. Unlike other forms of storage, writing to flash requires two +operations: erasing and programming. Programming (setting bits to 0) is +relatively cheap and can be very granular. Erasing however (setting bits to 1), +requires an expensive and destructive operation which gives flash its name. +[Wikipedia][wikipedia-flash] has more information on how exactly flash works. + +To make the situation more annoying, it's very common for these embedded +systems to lose power at any time. Usually, microcontroller code is simple and +reactive, with no concept of a shutdown routine. This presents a big challenge +for persistent storage, where an unlucky power loss can corrupt the storage and +leave a device unrecoverable. + +This leaves us with three major requirements for an embedded filesystem. + +1. **Power-loss resilience** - On these systems, power can be lost at any time. + If a power loss corrupts any persistent data structures, this can cause the + device to become unrecoverable. An embedded filesystem must be designed to + recover from a power loss during any write operation. + +1. **Wear leveling** - Writing to flash is destructive. If a filesystem + repeatedly writes to the same block, eventually that block will wear out. + Filesystems that don't take wear into account can easily burn through blocks + used to store frequently updated metadata and cause a device's early death. + +1. **Bounded RAM/ROM** - If the above requirements weren't enough, these + systems also have very limited amounts of memory. This prevents many + existing filesystem designs, which can lean on relatively large amounts of + RAM to temporarily store filesystem metadata. + + For ROM, this means we need to keep our design simple and reuse code paths + were possible. For RAM we have a stronger requirement, all RAM usage is + bounded. This means RAM usage does not grow as the filesystem changes in + size or number of files. This creates a unique challenge as even presumably + simple operations, such as traversing the filesystem, become surprisingly + difficult. + +## Existing designs? + +So, what's already out there? There are, of course, many different filesystems, +however they often share and borrow feature from each other. If we look at +power-loss resilience and wear leveling, we can narrow these down to a handful +of designs. + +1. First we have the non-resilient, block based filesystems, such as [FAT] and + [ext2]. These are the earliest filesystem designs and often the most simple. + Here storage is divided into blocks, with each file being stored in a + collection of blocks. Without modifications, these filesystems are not + power-loss resilient, so updating a file is a simple as rewriting the blocks + in place. + + ``` + .--------. + | root | + | | + | | + '--------' + .-' '-. + v v + .--------. .--------. + | A | | B | + | | | | + | | | | + '--------' '--------' + .-' .-' '-. + v v v + .--------. .--------. .--------. + | C | | D | | E | + | | | | | | + | | | | | | + '--------' '--------' '--------' + ``` + + Because of their simplicity, these filesystems are usually both the fastest + and smallest. However the lack of power resilience is not great, and the + binding relationship of storage location and data removes the filesystem's + ability to manage wear. + +2. In a completely different direction, we have logging filesystems, such as + [JFFS], [YAFFS], and [SPIFFS], storage location is not bound to a piece of + data, instead the entire storage is used for a circular log which is + appended with every change made to the filesystem. Writing appends new + changes, while reading requires traversing the log to reconstruct a file. + Some logging filesystems cache files to avoid the read cost, but this comes + at a tradeoff of RAM. + + ``` + v + .--------.--------.--------.--------.--------.--------.--------.--------. + | C | new B | new A | | A | B | + | | | |-> | | | + | | | | | | | + '--------'--------'--------'--------'--------'--------'--------'--------' + ``` + + Logging filesystem are beautifully elegant. With a checksum, we can easily + detect power-loss and fall back to the previous state by ignoring failed + appends. And if that wasn't good enough, their cyclic nature means that + logging filesystems distribute wear across storage perfectly. + + The main downside is performance. If we look at garbage collection, the + process of cleaning up outdated data from the end of the log, I've yet to + see a pure logging filesystem that does not have one of these two costs: + + 1. _O(n²)_ runtime + 2. _O(n)_ RAM + + SPIFFS is a very interesting case here, as it uses the fact that repeated + programs to NOR flash is both atomic and masking. This is a very neat + solution, however it limits the type of storage you can support. + +3. Perhaps the most common type of filesystem, a journaling filesystem is the + offspring that happens when you mate a block based filesystem with a logging + filesystem. [ext4] and [NTFS] are good examples. Here, we take a normal + block based filesystem and add a bounded log where we note every change + before it occurs. + + ``` + journal + .--------.--------. + .--------. | C'| D'| | E'| + | root |-->| | |-> | | + | | | | | | | + | | '--------'--------' + '--------' + .-' '-. + v v + .--------. .--------. + | A | | B | + | | | | + | | | | + '--------' '--------' + .-' .-' '-. + v v v + .--------. .--------. .--------. + | C | | D | | E | + | | | | | | + | | | | | | + '--------' '--------' '--------' + ``` + + + This sort of filesystem takes the best from both worlds. Performance can be + as fast as a block based filesystem (though updating the journal does have + a small cost), and atomic updates to the journal allow the filesystem to + recover in the event of a power loss. + + Unfortunately, journaling filesystems have a couple of problems. They are + fairly complex, since there are effectively two filesystems running in + parallel, which comes with a code size cost. They also offer no protection + against wear because of the strong relationship between storage location + and data. + +4. Last but not least we have copy-on-write (COW) filesystems, such as + [btrfs] and [ZFS]. These are very similar to other block based filesystems, + but instead of updating block inplace, all updates are performed by creating + a copy with the changes and replacing any references to the old block with + our new block. This recursively pushes all of our problems upwards until we + reach the root of our filesystem, which is often stored in a very small log. + + ``` + .--------. .--------. + | root | write |new root| + | | ==> | | + | | | | + '--------' '--------' + .-' '-. | '-. + | .-------|------------------' v + v v v .--------. + .--------. .--------. | new B | + | A | | B | | | + | | | | | | + | | | | '--------' + '--------' '--------' .-' | + .-' .-' '-. .------------|------' + | | | | v + v v v v .--------. + .--------. .--------. .--------. | new D | + | C | | D | | E | | | + | | | | | | | | + | | | | | | '--------' + '--------' '--------' '--------' + ``` + + COW filesystems are interesting. They offer very similar performance to + block based filesystems while managing to pull off atomic updates without + storing data changes directly in a log. They even disassociate the storage + location of data, which creates an opportunity for wear leveling. + + Well, almost. The unbounded upwards movement of updates causes some + problems. Because updates to a COW filesystem don't stop until they've + reached the root, an update can cascade into a larger set of writes than + would be needed for the original data. On top of this, the upward motion + focuses these writes into the block, which can wear out much earlier than + the rest of the filesystem. + +## littlefs + +So what does littlefs do? + +If we look at existing filesystems, there are two interesting design patterns +that stand out, but each have their own set of problems. Logging, which +provides independent atomicity, has poor runtime performance. And COW data +structures, which perform well, push the atomicity problem upwards. + +Can we work around these limitations? + +Consider logging. It has either a _O(n²)_ runtime or _O(n)_ RAM cost. We +can't avoid these costs, _but_ if we put an upper bound on the size we can at +least prevent the theoretical cost from becoming problem. This relies on the +super secret computer science hack where you can pretend any algorithmic +complexity is _O(1)_ by bounding the input. + +In the case of COW data structures, we can try twisting the definition a bit. +Let's say that our COW structure doesn't copy after a single write, but instead +copies after _n_ writes. This doesn't change most COW properties (assuming you +can write atomically!), but what it does do is prevent the upward motion of +wear. This sort of copy-on-bounded-writes (CObW) still focuses wear, but at +each level we divide the propagation of wear by _n_. With a sufficiently +large _n_ (> branching factor) wear propagation is no longer a problem. + +See where this is going? Separate, logging and COW are imperfect solutions and +have weaknesses that limit their usefulness. But if we merge the two they can +mutually solve each other's limitations. + +This is the idea behind littlefs. At the sub-block level, littlefs is built +out of small, two block logs that provide atomic updates to metadata anywhere +on the filesystem. At the super-block level, littlefs is a CObW tree of blocks +that can be evicted on demand. + +``` + root + .--------.--------. + | A'| B'| | + | | |-> | + | | | | + '--------'--------' + .----' '--------------. + A v B v + .--------.--------. .--------.--------. + | C'| D'| | | E'|new| | + | | |-> | | | E'|-> | + | | | | | | | | + '--------'--------' '--------'--------' + .-' '--. | '------------------. + v v .-' v +.--------. .--------. v .--------. +| C | | D | .--------. write | new E | +| | | | | E | ==> | | +| | | | | | | | +'--------' '--------' | | '--------' + '--------' .-' | + .-' '-. .-------------|------' + v v v v + .--------. .--------. .--------. + | F | | G | | new F | + | | | | | | + | | | | | | + '--------' '--------' '--------' +``` + +There are still some minor issues. Small logs can be expensive in terms of +storage, in the worst case a small log costs 4x the size of the original data. +CObW structures require an efficient block allocator since allocation occurs +every _n_ writes. And there is still the challenge of keeping the RAM usage +constant. + +## Metadata pairs + +Metadata pairs are the backbone of littlefs. These are small, two block logs +that allow atomic updates anywhere in the filesystem. + +Why two blocks? Well, logs work by appending entries to a circular buffer +stored on disk. But remember that flash has limited write granularity. We can +incrementally program new data onto erased blocks, but we need to erase a full +block at a time. This means that in order for our circular buffer to work, we +need more than one block. + +We could make our logs larger than two blocks, but the next challenge is how +do we store references to these logs? Because the blocks themselves are erased +during writes, using a data structure to track these blocks is complicated. +The simple solution here is to store a two block addresses for every metadata +pair. This has the added advantage that we can change out blocks in the +metadata pair independently, and we don't reduce our block granularity for +other operations. + +In order to determine which metadata block is the most recent, we store a +revision count that we compare using [sequence arithmetic][wikipedia-sna] +(very handy for avoiding problems with integer overflow). Conveniently, this +revision count also gives us a rough idea of how many erases have occurred on +the block. + +``` +metadata pair pointer: {block 0, block 1} + | '--------------------. + '-. | +disk v v +.--------.--------.--------.--------.--------.--------.--------.--------. +| | |metadata| |metadata| | +| | |block 0 | |block 1 | | +| | | | | | | +'--------'--------'--------'--------'--------'--------'--------'--------' + '--. .----' + v v + metadata pair .----------------.----------------. + | revision 11 | revision 12 | + block 1 is |----------------|----------------| + most recent | A | A'' | + |----------------|----------------| + | checksum | checksum | + |----------------|----------------| + | B | A''' | <- most recent A + |----------------|----------------| + | A'' | checksum | + |----------------|----------------| + | checksum | | | + |----------------| v | + '----------------'----------------' +``` + +So how do we atomically update our metadata pairs? Atomicity (a type of +power-loss resilience) requires two parts: redundancy and error detection. +Error detection can be provided with a checksum, and in littlefs's case we +use a 32-bit [CRC][wikipedia-crc]. Maintaining redundancy, on the other hand, +requires multiple stages. + +1. If our block is not full and the program size is small enough to let us + append more entries, we can simply append the entries to the log. Because + we don't overwrite the original entries (remember rewriting flash requires + an erase), we still have the original entries if we lose power during the + append. + + ``` + commit A + .----------------.----------------. .----------------.----------------. + | revision 1 | revision 0 | => | revision 1 | revision 0 | + |----------------|----------------| |----------------|----------------| + | | | | | A | | + | v | | |----------------| | + | | | | checksum | | + | | | |----------------| | + | | | | | | | + | | | | v | | + | | | | | | + | | | | | | + | | | | | | + | | | | | | + '----------------'----------------' '----------------'----------------' + ``` + + Note that littlefs doesn't maintain a checksum for each entry. Many logging + filesystems do this, but it limits what you can update in a single atomic + operation. What we can do instead is group multiple entries into a commit + that shares a single checksum. This lets us update multiple unrelated pieces + of metadata as long as they reside on the same metadata pair. + + ``` + commit B and A' + .----------------.----------------. .----------------.----------------. + | revision 1 | revision 0 | => | revision 1 | revision 0 | + |----------------|----------------| |----------------|----------------| + | A | | | A | | + |----------------| | |----------------| | + | checksum | | | checksum | | + |----------------| | |----------------| | + | | | | | B | | + | v | | |----------------| | + | | | | A' | | + | | | |----------------| | + | | | | checksum | | + | | | |----------------| | + '----------------'----------------' '----------------'----------------' + ``` + +2. If our block _is_ full of entries, we need to somehow remove outdated + entries to make space for new ones. This process is called garbage + collection, but because littlefs has multiple garbage collectors, we + also call this specific case compaction. + + Compared to other filesystems, littlefs's garbage collector is relatively + simple. We want to avoid RAM consumption, so we use a sort of brute force + solution where for each entry we check to see if a newer entry has been + written. If the entry is the most recent we append it to our new block. This + is where having two blocks becomes important, if we lose power we still have + everything in our original block. + + During this compaction step we also erase the metadata block and increment + the revision count. Because we can commit multiple entries at once, we can + write all of these changes to the second block without worrying about power + loss. It's only when the commit's checksum is written that the compacted + entries and revision count become committed and readable. + + ``` + commit B', need to compact + .----------------.----------------. .----------------.----------------. + | revision 1 | revision 0 | => | revision 1 | revision 2 | + |----------------|----------------| |----------------|----------------| + | A | | | A | A' | + |----------------| | |----------------|----------------| + | checksum | | | checksum | B' | + |----------------| | |----------------|----------------| + | B | | | B | checksum | + |----------------| | |----------------|----------------| + | A' | | | A' | | | + |----------------| | |----------------| v | + | checksum | | | checksum | | + |----------------| | |----------------| | + '----------------'----------------' '----------------'----------------' + ``` + +3. If our block is full of entries _and_ we can't find any garbage, then what? + At this point, most logging filesystems would return an error indicating no + more space is available, but because we have small logs, overflowing a log + isn't really an error condition. + + Instead, we split our original metadata pair into two metadata pairs, each + containing half of the entries, connected by a tail pointer. Instead of + increasing the size of the log and dealing with the scalability issues + associated with larger logs, we form a linked list of small bounded logs. + This is a tradeoff as this approach does use more storage space, but at the + benefit of improved scalability. + + Despite writing to two metadata pairs, we can still maintain power + resilience during this split step by first preparing the new metadata pair, + and then inserting the tail pointer during the commit to the original + metadata pair. + + ``` + commit C and D, need to split + .----------------.----------------. .----------------.----------------. + | revision 1 | revision 2 | => | revision 3 | revision 2 | + |----------------|----------------| |----------------|----------------| + | A | A' | | A' | A' | + |----------------|----------------| |----------------|----------------| + | checksum | B' | | B' | B' | + |----------------|----------------| |----------------|----------------| + | B | checksum | | tail ---------------------. + |----------------|----------------| |----------------|----------------| | + | A' | | | | checksum | | | + |----------------| v | |----------------| | | + | checksum | | | | | | | + |----------------| | | v | | | + '----------------'----------------' '----------------'----------------' | + .----------------.---------' + v v + .----------------.----------------. + | revision 1 | revision 0 | + |----------------|----------------| + | C | | + |----------------| | + | D | | + |----------------| | + | checksum | | + |----------------| | + | | | | + | v | | + | | | + | | | + '----------------'----------------' + ``` + +There is another complexity the crops up when dealing with small logs. The +amortized runtime cost of garbage collection is not only dependent on its +one time cost (_O(n²)_ for littlefs), but also depends on how often +garbage collection occurs. + +Consider two extremes: + +1. Log is empty, garbage collection occurs once every _n_ updates +2. Log is full, garbage collection occurs **every** update + +Clearly we need to be more aggressive than waiting for our metadata pair to +be full. As the metadata pair approaches fullness the frequency of compactions +grows very rapidly. + +Looking at the problem generically, consider a log with ![n] bytes for each +entry, ![d] dynamic entries (entries that are outdated during garbage +collection), and ![s] static entries (entries that need to be copied during +garbage collection). If we look at the amortized runtime complexity of updating +this log we get this formula: + +![cost = n + n (s / d+1)][metadata-formula1] + +If we let ![r] be the ratio of static space to the size of our log in bytes, we +find an alternative representation of the number of static and dynamic entries: + +![s = r (size/n)][metadata-formula2] + +![d = (1 - r) (size/n)][metadata-formula3] + +Substituting these in for ![d] and ![s] gives us a nice formula for the cost of +updating an entry given how full the log is: + +![cost = n + n (r (size/n) / ((1-r) (size/n) + 1))][metadata-formula4] + +Assuming 100 byte entries in a 4 KiB log, we can graph this using the entry +size to find a multiplicative cost: + +![Metadata pair update cost graph][metadata-cost-graph] + +So at 50% usage, we're seeing an average of 2x cost per update, and at 75% +usage, we're already at an average of 4x cost per update. + +To avoid this exponential growth, instead of waiting for our metadata pair +to be full, we split the metadata pair once we exceed 50% capacity. We do this +lazily, waiting until we need to compact before checking if we fit in our 50% +limit. This limits the overhead of garbage collection to 2x the runtime cost, +giving us an amortized runtime complexity of _O(1)_. + +--- + +If we look at metadata pairs and linked-lists of metadata pairs at a high +level, they have fairly nice runtime costs. Assuming _n_ metadata pairs, +each containing _m_ metadata entries, the _lookup_ cost for a specific +entry has a worst case runtime complexity of _O(nm)_. For _updating_ a specific +entry, the worst case complexity is _O(nm²)_, with an amortized complexity +of only _O(nm)_. + +However, splitting at 50% capacity does mean that in the best case our +metadata pairs will only be 1/2 full. If we include the overhead of the second +block in our metadata pair, each metadata entry has an effective storage cost +of 4x the original size. I imagine users would not be happy if they found +that they can only use a quarter of their original storage. Metadata pairs +provide a mechanism for performing atomic updates, but we need a separate +mechanism for storing the bulk of our data. + +## CTZ skip-lists + +Metadata pairs provide efficient atomic updates but unfortunately have a large +storage cost. But we can work around this storage cost by only using the +metadata pairs to store references to more dense, copy-on-write (COW) data +structures. + +[Copy-on-write data structures][wikipedia-cow], also called purely functional +data structures, are a category of data structures where the underlying +elements are immutable. Making changes to the data requires creating new +elements containing a copy of the updated data and replacing any references +with references to the new elements. Generally, the performance of a COW data +structure depends on how many old elements can be reused after replacing parts +of the data. + +littlefs has several requirements of its COW structures. They need to be +efficient to read and write, but most frustrating, they need to be traversable +with a constant amount of RAM. Notably this rules out +[B-trees][wikipedia-B-tree], which can not be traversed with constant RAM, and +[B+-trees][wikipedia-B+-tree], which are not possible to update with COW +operations. + +--- + +So, what can we do? First let's consider storing files in a simple COW +linked-list. Appending a block, which is the basis for writing files, means we +have to update the last block to point to our new block. This requires a COW +operation, which means we need to update the second-to-last block, and then the +third-to-last, and so on until we've copied out the entire file. + +``` +A linked-list +.--------. .--------. .--------. .--------. .--------. .--------. +| data 0 |->| data 1 |->| data 2 |->| data 4 |->| data 5 |->| data 6 | +| | | | | | | | | | | | +| | | | | | | | | | | | +'--------' '--------' '--------' '--------' '--------' '--------' +``` + +To avoid a full copy during appends, we can store the data backwards. Appending +blocks just requires adding the new block and no other blocks need to be +updated. If we update a block in the middle, we still need to copy the +following blocks, but can reuse any blocks before it. Since most file writes +are linear, this design gambles that appends are the most common type of data +update. + +``` +A backwards linked-list +.--------. .--------. .--------. .--------. .--------. .--------. +| data 0 |<-| data 1 |<-| data 2 |<-| data 4 |<-| data 5 |<-| data 6 | +| | | | | | | | | | | | +| | | | | | | | | | | | +'--------' '--------' '--------' '--------' '--------' '--------' +``` + +However, a backwards linked-list does have a rather glaring problem. Iterating +over a file _in order_ has a runtime cost of _O(n²)_. A quadratic runtime +just to read a file! That's awful. + +Fortunately we can do better. Instead of a singly linked list, littlefs +uses a multilayered linked-list often called a +[skip-list][wikipedia-skip-list]. However, unlike the most common type of +skip-list, littlefs's skip-lists are strictly deterministic built around some +interesting properties of the count-trailing-zeros (CTZ) instruction. + +The rules CTZ skip-lists follow are that for every _n_‍th block where _n_ +is divisible by 2‍_ˣ_, that block contains a pointer to block +_n_-2‍_ˣ_. This means that each block contains anywhere from 1 to +log₂_n_ pointers that skip to different preceding elements of the +skip-list. + +The name comes from heavy use of the [CTZ instruction][wikipedia-ctz], which +lets us calculate the power-of-two factors efficiently. For a give block _n_, +that block contains ctz(_n_)+1 pointers. + +``` +A backwards CTZ skip-list +.--------. .--------. .--------. .--------. .--------. .--------. +| data 0 |<-| data 1 |<-| data 2 |<-| data 3 |<-| data 4 |<-| data 5 | +| |<-| |--| |<-| |--| | | | +| |<-| |--| |--| |--| | | | +'--------' '--------' '--------' '--------' '--------' '--------' +``` + +The additional pointers let us navigate the data-structure on disk much more +efficiently than in a singly linked list. + +Consider a path from data block 5 to data block 1. You can see how data block 3 +was completely skipped: +``` +.--------. .--------. .--------. .--------. .--------. .--------. +| data 0 | | data 1 |<-| data 2 | | data 3 | | data 4 |<-| data 5 | +| | | | | |<-| |--| | | | +| | | | | | | | | | | | +'--------' '--------' '--------' '--------' '--------' '--------' +``` + +The path to data block 0 is even faster, requiring only two jumps: +``` +.--------. .--------. .--------. .--------. .--------. .--------. +| data 0 | | data 1 | | data 2 | | data 3 | | data 4 |<-| data 5 | +| | | | | | | | | | | | +| |<-| |--| |--| |--| | | | +'--------' '--------' '--------' '--------' '--------' '--------' +``` + +We can find the runtime complexity by looking at the path to any block from +the block containing the most pointers. Every step along the path divides +the search space for the block in half, giving us a runtime of _O(log n)_. +To get _to_ the block with the most pointers, we can perform the same steps +backwards, which puts the runtime at _O(2 log n)_ = _O(log n)_. An interesting +note is that this optimal path occurs naturally if we greedily choose the +pointer that covers the most distance without passing our target. + +So now we have a [COW] data structure that is cheap to append with a runtime +of _O(1)_, and can be read with a worst case runtime of _O(n log n)_. Given +that this runtime is also divided by the amount of data we can store in a +block, this cost is fairly reasonable. + +--- + +This is a new data structure, so we still have several questions. What is the +storage overhead? Can the number of pointers exceed the size of a block? How do +we store a CTZ skip-list in our metadata pairs? + +To find the storage overhead, we can look at the data structure as multiple +linked-lists. Each linked-list skips twice as many blocks as the previous, +or from another perspective, each linked-list uses half as much storage as +the previous. As we approach infinity, the storage overhead forms a geometric +series. Solving this tells us that on average our storage overhead is only +2 pointers per block. + +![lim,n->inf((1/n)sum,i,0->n(ctz(i)+1)) = sum,i,0->inf(1/2^i) = 2][ctz-formula1] + +Because our file size is limited the word width we use to store sizes, we can +also solve for the maximum number of pointers we would ever need to store in a +block. If we set the overhead of pointers equal to the block size, we get the +following equation. Note that both a smaller block size (![B][bigB]) and larger +word width (![w]) result in more storage overhead. + +![B = (w/8)ceil(log2(2^w / (B-2w/8)))][ctz-formula2] + +Solving the equation for ![B][bigB] gives us the minimum block size for some +common word widths: + +1. 32-bit CTZ skip-list => minimum block size of 104 bytes +2. 64-bit CTZ skip-list => minimum block size of 448 bytes + +littlefs uses a 32-bit word width, so our blocks can only overflow with +pointers if they are smaller than 104 bytes. This is an easy requirement, as +in practice, most block sizes start at 512 bytes. As long as our block size +is larger than 104 bytes, we can avoid the extra logic needed to handle +pointer overflow. + +This last question is how do we store CTZ skip-lists? We need a pointer to the +head block, the size of the skip-list, the index of the head block, and our +offset in the head block. But it's worth noting that each size maps to a unique +index + offset pair. So in theory we can store only a single pointer and size. + +However, calculating the index + offset pair from the size is a bit +complicated. We can start with a summation that loops through all of the blocks +up until our given size. Let ![B][bigB] be the block size in bytes, ![w] be the +word width in bits, ![n] be the index of the block in the skip-list, and +![N][bigN] be the file size in bytes: + +![N = sum,i,0->n(B-(w/8)(ctz(i)+1))][ctz-formula3] + +This works quite well, but requires _O(n)_ to compute, which brings the full +runtime of reading a file up to _O(n² log n)_. Fortunately, that summation +doesn't need to touch the disk, so the practical impact is minimal. + +However, despite the integration of a bitwise operation, we can actually reduce +this equation to a _O(1)_ form. While browsing the amazing resource that is +the [On-Line Encyclopedia of Integer Sequences (OEIS)][oeis], I managed to find +[A001511], which matches the iteration of the CTZ instruction, +and [A005187], which matches its partial summation. Much to my +surprise, these both result from simple equations, leading us to a rather +unintuitive property that ties together two seemingly unrelated bitwise +instructions: + +![sum,i,0->n(ctz(i)+1) = 2n-popcount(n)][ctz-formula4] + +where: + +1. ctz(![x]) = the number of trailing bits that are 0 in ![x] +2. popcount(![x]) = the number of bits that are 1 in ![x] + +Initial tests of this surprising property seem to hold. As ![n] approaches +infinity, we end up with an average overhead of 2 pointers, which matches our +assumption from earlier. During iteration, the popcount function seems to +handle deviations from this average. Of course, just to make sure I wrote a +quick script that verified this property for all 32-bit integers. + +Now we can substitute into our original equation to find a more efficient +equation for file size: + +![N = Bn - (w/8)(2n-popcount(n))][ctz-formula5] + +Unfortunately, the popcount function is non-injective, so we can't solve this +equation for our index. But what we can do is solve for an ![n'] index that +is greater than ![n] with error bounded by the range of the popcount function. +We can repeatedly substitute ![n'] into the original equation until the error +is smaller than our integer resolution. As it turns out, we only need to +perform this substitution once, which gives us this formula for our index: + +![n = floor((N-(w/8)popcount(N/(B-2w/8))) / (B-2w/8))][ctz-formula6] + +Now that we have our index ![n], we can just plug it back into the above +equation to find the offset. We run into a bit of a problem with integer +overflow, but we can avoid this by rearranging the equation a bit: + +![off = N - (B-2w/8)n - (w/8)popcount(n)][ctz-formula7] + +Our solution requires quite a bit of math, but computers are very good at math. +Now we can find both our block index and offset from a size in _O(1)_, letting +us store CTZ skip-lists with only a pointer and size. + +CTZ skip-lists give us a COW data structure that is easily traversable in +_O(n)_, can be appended in _O(1)_, and can be read in _O(n log n)_. All of +these operations work in a bounded amount of RAM and require only two words of +storage overhead per block. In combination with metadata pairs, CTZ skip-lists +provide power resilience and compact storage of data. + +``` + .--------. + .|metadata| + || | + || | + |'--------' + '----|---' + v +.--------. .--------. .--------. .--------. +| data 0 |<-| data 1 |<-| data 2 |<-| data 3 | +| |<-| |--| | | | +| | | | | | | | +'--------' '--------' '--------' '--------' + +write data to disk, create copies +=> + .--------. + .|metadata| + || | + || | + |'--------' + '----|---' + v +.--------. .--------. .--------. .--------. +| data 0 |<-| data 1 |<-| data 2 |<-| data 3 | +| |<-| |--| | | | +| | | | | | | | +'--------' '--------' '--------' '--------' + ^ ^ ^ + | | | .--------. .--------. .--------. .--------. + | | '----| new |<-| new |<-| new |<-| new | + | '----------------| data 2 |<-| data 3 |--| data 4 | | data 5 | + '------------------| |--| |--| | | | + '--------' '--------' '--------' '--------' + +commit to metadata pair +=> + .--------. + .|new | + ||metadata| + || | + |'--------' + '----|---' + | +.--------. .--------. .--------. .--------. | +| data 0 |<-| data 1 |<-| data 2 |<-| data 3 | | +| |<-| |--| | | | | +| | | | | | | | | +'--------' '--------' '--------' '--------' | + ^ ^ ^ v + | | | .--------. .--------. .--------. .--------. + | | '----| new |<-| new |<-| new |<-| new | + | '----------------| data 2 |<-| data 3 |--| data 4 | | data 5 | + '------------------| |--| |--| | | | + '--------' '--------' '--------' '--------' +``` + +## The block allocator + +So we now have the framework for an atomic, wear leveling filesystem. Small two +block metadata pairs provide atomic updates, while CTZ skip-lists provide +compact storage of data in COW blocks. + +But now we need to look at the [elephant] in the room. Where do all these +blocks come from? + +Deciding which block to use next is the responsibility of the block allocator. +In filesystem design, block allocation is often a second-class citizen, but in +a COW filesystem its role becomes much more important as it is needed for +nearly every write to the filesystem. + +Normally, block allocation involves some sort of free list or bitmap stored on +the filesystem that is updated with free blocks. However, with power +resilience, keeping these structures consistent becomes difficult. It doesn't +help that any mistake in updating these structures can result in lost blocks +that are impossible to recover. + +littlefs takes a cautious approach. Instead of trusting a free list on disk, +littlefs relies on the fact that the filesystem on disk is a mirror image of +the free blocks on the disk. The block allocator operates much like a garbage +collector in a scripting language, scanning for unused blocks on demand. + +``` + .----. + |root| + | | + '----' + v-------' '-------v +.----. . . .----. +| A | . . | B | +| | . . | | +'----' . . '----' +. . . . v--' '------------v---------v +. . . .----. . .----. .----. +. . . | C | . | D | | E | +. . . | | . | | | | +. . . '----' . '----' '----' +. . . . . . . . . . +.----.----.----.----.----.----.----.----.----.----.----.----. +| A | |root| C | B | | D | | E | | +| | | | | | | | | | | +'----'----'----'----'----'----'----'----'----'----'----'----' + ^ ^ ^ ^ ^ + '-------------------'----'-------------------'----'-- free blocks +``` + +While this approach may sound complicated, the decision to not maintain a free +list greatly simplifies the overall design of littlefs. Unlike programming +languages, there are only a handful of data structures we need to traverse. +And block deallocation, which occurs nearly as often as block allocation, +is simply a noop. This "drop it on the floor" strategy greatly reduces the +complexity of managing on disk data structures, especially when handling +high-risk error conditions. + +--- + +Our block allocator needs to find free blocks efficiently. You could traverse +through every block on storage and check each one against our filesystem tree; +however, the runtime would be abhorrent. We need to somehow collect multiple +blocks per traversal. + +Looking at existing designs, some larger filesystems that use a similar "drop +it on the floor" strategy store a bitmap of the entire storage in [RAM]. This +works well because bitmaps are surprisingly compact. We can't use the same +strategy here, as it violates our constant RAM requirement, but we may be able +to modify the idea into a workable solution. + +``` +.----.----.----.----.----.----.----.----.----.----.----.----. +| A | |root| C | B | | D | | E | | +| | | | | | | | | | | +'----'----'----'----'----'----'----'----'----'----'----'----' + 1 0 1 1 1 0 0 1 0 1 0 0 + \---------------------------+----------------------------/ + v + bitmap: 0xb94 (0b101110010100) +``` + +The block allocator in littlefs is a compromise between a disk-sized bitmap and +a brute force traversal. Instead of a bitmap the size of storage, we keep track +of a small, fixed-size bitmap called the lookahead buffer. During block +allocation, we take blocks from the lookahead buffer. If the lookahead buffer +is empty, we scan the filesystem for more free blocks, populating our lookahead +buffer. In each scan we use an increasing offset, circling the storage as +blocks are allocated. + +Here's what it might look like to allocate 4 blocks on a decently busy +filesystem with a 32 bit lookahead and a total of 128 blocks (512 KiB +of storage if blocks are 4 KiB): +``` +boot... lookahead: + fs blocks: fffff9fffffffffeffffffffffff0000 +scanning... lookahead: fffff9ff + fs blocks: fffff9fffffffffeffffffffffff0000 +alloc = 21 lookahead: fffffdff + fs blocks: fffffdfffffffffeffffffffffff0000 +alloc = 22 lookahead: ffffffff + fs blocks: fffffffffffffffeffffffffffff0000 +scanning... lookahead: fffffffe + fs blocks: fffffffffffffffeffffffffffff0000 +alloc = 63 lookahead: ffffffff + fs blocks: ffffffffffffffffffffffffffff0000 +scanning... lookahead: ffffffff + fs blocks: ffffffffffffffffffffffffffff0000 +scanning... lookahead: ffffffff + fs blocks: ffffffffffffffffffffffffffff0000 +scanning... lookahead: ffff0000 + fs blocks: ffffffffffffffffffffffffffff0000 +alloc = 112 lookahead: ffff8000 + fs blocks: ffffffffffffffffffffffffffff8000 +``` + +This lookahead approach has a runtime complexity of _O(n²)_ to completely +scan storage; however, bitmaps are surprisingly compact, and in practice only +one or two passes are usually needed to find free blocks. Additionally, the +performance of the allocator can be optimized by adjusting the block size or +size of the lookahead buffer, trading either write granularity or RAM for +allocator performance. + +## Wear leveling + +The block allocator has a secondary role: wear leveling. + +Wear leveling is the process of distributing wear across all blocks in the +storage to prevent the filesystem from experiencing an early death due to +wear on a single block in the storage. + +littlefs has two methods of protecting against wear: +1. Detection and recovery from bad blocks +2. Evenly distributing wear across dynamic blocks + +--- + +Recovery from bad blocks doesn't actually have anything to do with the block +allocator itself. Instead, it relies on the ability of the filesystem to detect +and evict bad blocks when they occur. + +In littlefs, it is fairly straightforward to detect bad blocks at write time. +All writes must be sourced by some form of data in RAM, so immediately after we +write to a block, we can read the data back and verify that it was written +correctly. If we find that the data on disk does not match the copy we have in +RAM, a write error has occurred and we most likely have a bad block. + +Once we detect a bad block, we need to recover from it. In the case of write +errors, we have a copy of the corrupted data in RAM, so all we need to do is +evict the bad block, allocate a new, hopefully good block, and repeat the write +that previously failed. + +The actual act of evicting the bad block and replacing it with a new block is +left up to the filesystem's copy-on-bounded-writes (CObW) data structures. One +property of CObW data structures is that any block can be replaced during a +COW operation. The bounded-writes part is normally triggered by a counter, but +nothing prevents us from triggering a COW operation as soon as we find a bad +block. + +``` + .----. + |root| + | | + '----' + v--' '----------------------v +.----. .----. +| A | | B | +| | | | +'----' '----' +. . v---' . +. . .----. . +. . | C | . +. . | | . +. . '----' . +. . . . . +.----.----.----.----.----.----.----.----.----.----. +| A |root| | C | B | | +| | | | | | | +'----'----'----'----'----'----'----'----'----'----' + +update C +=> + .----. + |root| + | | + '----' + v--' '----------------------v +.----. .----. +| A | | B | +| | | | +'----' '----' +. . v---' . +. . .----. . +. . |bad | . +. . |blck| . +. . '----' . +. . . . . +.----.----.----.----.----.----.----.----.----.----. +| A |root| |bad | B | | +| | | |blck| | | +'----'----'----'----'----'----'----'----'----'----' + +oh no! bad block! relocate C +=> + .----. + |root| + | | + '----' + v--' '----------------------v +.----. .----. +| A | | B | +| | | | +'----' '----' +. . v---' . +. . .----. . +. . |bad | . +. . |blck| . +. . '----' . +. . . . . +.----.----.----.----.----.----.----.----.----.----. +| A |root| |bad | B |bad | | +| | | |blck| |blck| | +'----'----'----'----'----'----'----'----'----'----' + ---------> +oh no! bad block! relocate C +=> + .----. + |root| + | | + '----' + v--' '----------------------v +.----. .----. +| A | | B | +| | | | +'----' '----' +. . v---' . +. . .----. . .----. +. . |bad | . | C' | +. . |blck| . | | +. . '----' . '----' +. . . . . . . +.----.----.----.----.----.----.----.----.----.----. +| A |root| |bad | B |bad | C' | | +| | | |blck| |blck| | | +'----'----'----'----'----'----'----'----'----'----' + --------------> +successfully relocated C, update B +=> + .----. + |root| + | | + '----' + v--' '----------------------v +.----. .----. +| A | |bad | +| | |blck| +'----' '----' +. . v---' . +. . .----. . .----. +. . |bad | . | C' | +. . |blck| . | | +. . '----' . '----' +. . . . . . . +.----.----.----.----.----.----.----.----.----.----. +| A |root| |bad |bad |bad | C' | | +| | | |blck|blck|blck| | | +'----'----'----'----'----'----'----'----'----'----' + +oh no! bad block! relocate B +=> + .----. + |root| + | | + '----' + v--' '----------------------v +.----. .----. .----. +| A | |bad | |bad | +| | |blck| |blck| +'----' '----' '----' +. . v---' . . . +. . .----. . .----. . +. . |bad | . | C' | . +. . |blck| . | | . +. . '----' . '----' . +. . . . . . . . +.----.----.----.----.----.----.----.----.----.----. +| A |root| |bad |bad |bad | C' |bad | +| | | |blck|blck|blck| |blck| +'----'----'----'----'----'----'----'----'----'----' + --------------> +oh no! bad block! relocate B +=> + .----. + |root| + | | + '----' + v--' '----------------------v +.----. .----. .----. +| A | | B' | |bad | +| | | | |blck| +'----' '----' '----' +. . . | . .---' . +. . . '--------------v-------------v +. . . . .----. . .----. +. . . . |bad | . | C' | +. . . . |blck| . | | +. . . . '----' . '----' +. . . . . . . . . +.----.----.----.----.----.----.----.----.----.----. +| A |root| B' | |bad |bad |bad | C' |bad | +| | | | |blck|blck|blck| |blck| +'----'----'----'----'----'----'----'----'----'----' +------------> ------------------ +successfully relocated B, update root +=> + .----. + |root| + | | + '----' + v--' '--v +.----. .----. +| A | | B' | +| | | | +'----' '----' +. . . '---------------------------v +. . . . .----. +. . . . | C' | +. . . . | | +. . . . '----' +. . . . . . +.----.----.----.----.----.----.----.----.----.----. +| A |root| B' | |bad |bad |bad | C' |bad | +| | | | |blck|blck|blck| |blck| +'----'----'----'----'----'----'----'----'----'----' +``` + +We may find that the new block is also bad, but hopefully after repeating this +cycle we'll eventually find a new block where a write succeeds. If we don't, +that means that all blocks in our storage are bad, and we've reached the end of +our device's usable life. At this point, littlefs will return an "out of space" +error. This is technically true, as there are no more good blocks, but as an +added benefit it also matches the error condition expected by users of +dynamically sized data. + +--- + +Read errors, on the other hand, are quite a bit more complicated. We don't have +a copy of the data lingering around in RAM, so we need a way to reconstruct the +original data even after it has been corrupted. One such mechanism for this is +[error-correction-codes (ECC)][wikipedia-ecc]. + +ECC is an extension to the idea of a checksum. Where a checksum such as CRC can +detect that an error has occurred in the data, ECC can detect and actually +correct some amount of errors. However, there is a limit to how many errors ECC +can detect: the [Hamming bound][wikipedia-hamming-bound]. As the number of +errors approaches the Hamming bound, we may still be able to detect errors, but +can no longer fix the data. If we've reached this point the block is +unrecoverable. + +littlefs by itself does **not** provide ECC. The block nature and relatively +large footprint of ECC does not work well with the dynamically sized data of +filesystems, correcting errors without RAM is complicated, and ECC fits better +with the geometry of block devices. In fact, several NOR flash chips have extra +storage intended for ECC, and many NAND chips can even calculate ECC on the +chip itself. + +In littlefs, ECC is entirely optional. Read errors can instead be prevented +proactively by wear leveling. But it's important to note that ECC can be used +at the block device level to modestly extend the life of a device. littlefs +respects any errors reported by the block device, allowing a block device to +provide additional aggressive error detection. + +--- + +To avoid read errors, we need to be proactive, as opposed to reactive as we +were with write errors. + +One way to do this is to detect when the number of errors in a block exceeds +some threshold, but is still recoverable. With ECC we can do this at write +time, and treat the error as a write error, evicting the block before fatal +read errors have a chance to develop. + +A different, more generic strategy, is to proactively distribute wear across +all blocks in the storage, with the hope that no single block fails before the +rest of storage is approaching the end of its usable life. This is called +wear leveling. + +Generally, wear leveling algorithms fall into one of two categories: + +1. [Dynamic wear leveling][wikipedia-dynamic-wear-leveling], where we + distribute wear over "dynamic" blocks. The can be accomplished by + only considering unused blocks. + +2. [Static wear leveling][wikipedia-static-wear-leveling], where we + distribute wear over both "dynamic" and "static" blocks. To make this work, + we need to consider all blocks, including blocks that already contain data. + +As a tradeoff for code size and complexity, littlefs (currently) only provides +dynamic wear leveling. This is a best effort solution. Wear is not distributed +perfectly, but it is distributed among the free blocks and greatly extends the +life of a device. + +On top of this, littlefs uses a statistical wear leveling algorithm. What this +means is that we don’t actively track wear, instead we rely on a uniform +distribution of wear across storage to approximate a dynamic wear leveling +algorithm. Despite the long name, this is actually a simplification of dynamic +wear leveling. + +The uniform distribution of wear is left up to the block allocator, which +creates a uniform distribution in two parts. The easy part is when the device +is powered, in which case we allocate the blocks linearly, circling the device. +The harder part is what to do when the device loses power. We can't just +restart the allocator at the beginning of storage, as this would bias the wear. +Instead, we start the allocator as a random offset every time we mount the +filesystem. As long as this random offset is uniform, the combined allocation +pattern is also a uniform distribution. + +![Cumulative wear distribution graph][wear-distribution-graph] + +Initially, this approach to wear leveling looks like it creates a difficult +dependency on a power-independent random number generator, which must return +different random numbers on each boot. However, the filesystem is in a +relatively unique situation in that it is sitting on top of a large of amount +of entropy that persists across power loss. + +We can actually use the data on disk to directly drive our random number +generator. In practice, this is implemented by xoring the checksums of each +metadata pair, which is already calculated to fetch and mount the filesystem. + +``` + .--------. \ probably random + .|metadata| | ^ + || | +-> crc ----------------------> xor + || | | ^ + |'--------' / | + '---|--|-' | + .-' '-------------------------. | + | | | + | .--------------> xor ------------> xor + | | ^ | ^ + v crc crc v crc + .--------. \ ^ .--------. \ ^ .--------. \ ^ + .|metadata|-|--|-->|metadata| | | .|metadata| | | + || | +--' || | +--' || | +--' + || | | || | | || | | + |'--------' / |'--------' / |'--------' / + '---|--|-' '----|---' '---|--|-' + .-' '-. | .-' '-. + v v v v v +.--------. .--------. .--------. .--------. .--------. +| data | | data | | data | | data | | data | +| | | | | | | | | | +| | | | | | | | | | +'--------' '--------' '--------' '--------' '--------' +``` + +Note that this random number generator is not perfect. It only returns unique +random numbers when the filesystem is modified. This is exactly what we want +for distributing wear in the allocator, but means this random number generator +is not useful for general use. + +--- + +Together, bad block detection and dynamic wear leveling provide a best effort +solution for avoiding the early death of a filesystem due to wear. Importantly, +littlefs's wear leveling algorithm provides a key feature: You can increase the +life of a device simply by increasing the size of storage. And if more +aggressive wear leveling is desired, you can always combine littlefs with a +[flash translation layer (FTL)][wikipedia-ftl] to get a small power resilient +filesystem with static wear leveling. + +## Files + +Now that we have our building blocks out of the way, we can start looking at +our filesystem as a whole. + +The first step: How do we actually store our files? + +We've determined that CTZ skip-lists are pretty good at storing data compactly, +so following the precedent found in other filesystems we could give each file +a skip-list stored in a metadata pair that acts as an inode for the file. + + +``` + .--------. + .|metadata| + || | + || | + |'--------' + '----|---' + v +.--------. .--------. .--------. .--------. +| data 0 |<-| data 1 |<-| data 2 |<-| data 3 | +| |<-| |--| | | | +| | | | | | | | +'--------' '--------' '--------' '--------' +``` + +However, this doesn't work well when files are small, which is common for +embedded systems. Compared to PCs, _all_ data in an embedded system is small. + +Consider a small 4-byte file. With a two block metadata-pair and one block for +the CTZ skip-list, we find ourselves using a full 3 blocks. On most NOR flash +with 4 KiB blocks, this is 12 KiB of overhead. A ridiculous 3072x increase. + +``` +file stored as inode, 4 bytes costs ~12 KiB + + .----------------. \ +.| revision | | +||----------------| \ | +|| skiplist ---. +- metadata | +||----------------| | / 4x8 bytes | +|| checksum | | 32 bytes | +||----------------| | | +|| | | | +- metadata pair +|| v | | | 2x4 KiB +|| | | | 8 KiB +|| | | | +|| | | | +|| | | | +|'----------------' | | +'----------------' | / + .--------' + v + .----------------. \ \ + | data | +- data | + |----------------| / 4 bytes | + | | | + | | | + | | | + | | +- data block + | | | 4 KiB + | | | + | | | + | | | + | | | + | | | + '----------------' / +``` + +We can make several improvements. First, instead of giving each file its own +metadata pair, we can store multiple files in a single metadata pair. One way +to do this is to directly associate a directory with a metadata pair (or a +linked list of metadata pairs). This makes it easy for multiple files to share +the directory's metadata pair for logging and reduces the collective storage +overhead. + +The strict binding of metadata pairs and directories also gives users +direct control over storage utilization depending on how they organize their +directories. + +``` +multiple files stored in metadata pair, 4 bytes costs ~4 KiB + + .----------------. + .| revision | + ||----------------| + || A name | + || A skiplist -----. + ||----------------| | \ + || B name | | +- metadata + || B skiplist ---. | | 4x8 bytes + ||----------------| | | / 32 bytes + || checksum | | | + ||----------------| | | + || | | | | + || v | | | + |'----------------' | | + '----------------' | | + .----------------' | + v v +.----------------. .----------------. \ \ +| A data | | B data | +- data | +| | |----------------| / 4 bytes | +| | | | | +| | | | | +| | | | | +| | | | + data block +| | | | | 4 KiB +| | | | | +|----------------| | | | +| | | | | +| | | | | +| | | | | +'----------------' '----------------' / +``` + +The second improvement we can make is noticing that for very small files, our +attempts to use CTZ skip-lists for compact storage backfires. Metadata pairs +have a ~4x storage cost, so if our file is smaller than 1/4 the block size, +there's actually no benefit in storing our file outside of our metadata pair. + +In this case, we can store the file directly in our directory's metadata pair. +We call this an inline file, and it allows a directory to store many small +files quite efficiently. Our previous 4 byte file now only takes up a +theoretical 16 bytes on disk. + +``` +inline files stored in metadata pair, 4 bytes costs ~16 bytes + + .----------------. +.| revision | +||----------------| +|| A name | +|| A skiplist ---. +||----------------| | \ +|| B name | | +- data +|| B data | | | 4x4 bytes +||----------------| | / 16 bytes +|| checksum | | +||----------------| | +|| | | | +|| v | | +|'----------------' | +'----------------' | + .---------' + v + .----------------. + | A data | + | | + | | + | | + | | + | | + | | + | | + |----------------| + | | + | | + | | + '----------------' +``` + +Once the file exceeds 1/4 the block size, we switch to a CTZ skip-list. This +means that our files never use more than 4x storage overhead, decreasing as +the file grows in size. + +![File storage cost graph][file-cost-graph] + +## Directories + +Now we just need directories to store our files. As mentioned above we want +a strict binding of directories and metadata pairs, but there are a few +complications we need to sort out. + +On their own, each directory is a linked-list of metadata pairs. This lets us +store an unlimited number of files in each directory, and we don't need to +worry about the runtime complexity of unbounded logs. We can store other +directory pointers in our metadata pairs, which gives us a directory tree, much +like what you find on other filesystems. + +``` + .--------. + .| root | + || | + || | + |'--------' + '---|--|-' + .-' '-------------------------. + v v + .--------. .--------. .--------. + .| dir A |------->| dir A | .| dir B | + || | || | || | + || | || | || | + |'--------' |'--------' |'--------' + '---|--|-' '----|---' '---|--|-' + .-' '-. | .-' '-. + v v v v v +.--------. .--------. .--------. .--------. .--------. +| file C | | file D | | file E | | file F | | file G | +| | | | | | | | | | +| | | | | | | | | | +'--------' '--------' '--------' '--------' '--------' +``` + +The main complication is, once again, traversal with a constant amount of +[RAM]. The directory tree is a tree, and the unfortunate fact is you can't +traverse a tree with constant RAM. + +Fortunately, the elements of our tree are metadata pairs, so unlike CTZ +skip-lists, we're not limited to strict COW operations. One thing we can do is +thread a linked-list through our tree, explicitly enabling cheap traversal +over the entire filesystem. + +``` + .--------. + .| root |-. + || | | + .-------|| |-' + | |'--------' + | '---|--|-' + | .-' '-------------------------. + | v v + | .--------. .--------. .--------. + '->| dir A |------->| dir A |------->| dir B | + || | || | || | + || | || | || | + |'--------' |'--------' |'--------' + '---|--|-' '----|---' '---|--|-' + .-' '-. | .-' '-. + v v v v v +.--------. .--------. .--------. .--------. .--------. +| file C | | file D | | file E | | file F | | file G | +| | | | | | | | | | +| | | | | | | | | | +'--------' '--------' '--------' '--------' '--------' +``` + +Unfortunately, not sticking to pure COW operations creates some problems. Now, +whenever we want to manipulate the directory tree, multiple pointers need to be +updated. If you're familiar with designing atomic data structures this should +set off a bunch of red flags. + +To work around this, our threaded linked-list has a bit of leeway. Instead of +only containing metadata pairs found in our filesystem, it is allowed to +contain metadata pairs that have no parent because of a power loss. These are +called orphaned metadata pairs. + +With the possibility of orphans, we can build power loss resilient operations +that maintain a filesystem tree threaded with a linked-list for traversal. + +Adding a directory to our tree: + +``` + .--------. + .| root |-. + || | | +.-------|| |-' +| |'--------' +| '---|--|-' +| .-' '-. +| v v +| .--------. .--------. +'->| dir A |->| dir C | + || | || | + || | || | + |'--------' |'--------' + '--------' '--------' + +allocate dir B +=> + .--------. + .| root |-. + || | | +.-------|| |-' +| |'--------' +| '---|--|-' +| .-' '-. +| v v +| .--------. .--------. +'->| dir A |--->| dir C | + || | .->| | + || | | || | + |'--------' | |'--------' + '--------' | '--------' + | + .--------. | + .| dir B |-' + || | + || | + |'--------' + '--------' + +insert dir B into threaded linked-list, creating an orphan +=> + .--------. + .| root |-. + || | | +.-------|| |-' +| |'--------' +| '---|--|-' +| .-' '-------------. +| v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || | || orphan!| || | + || | || | || | + |'--------' |'--------' |'--------' + '--------' '--------' '--------' + +add dir B to parent directory +=> + .--------. + .| root |-. + || | | +.-------------|| |-' +| |'--------' +| '--|-|-|-' +| .------' | '-------. +| v v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || | || | || | + || | || | || | + |'--------' |'--------' |'--------' + '--------' '--------' '--------' +``` + +Removing a directory: + +``` + .--------. + .| root |-. + || | | +.-------------|| |-' +| |'--------' +| '--|-|-|-' +| .------' | '-------. +| v v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || | || | || | + || | || | || | + |'--------' |'--------' |'--------' + '--------' '--------' '--------' + +remove dir B from parent directory, creating an orphan +=> + .--------. + .| root |-. + || | | +.-------|| |-' +| |'--------' +| '---|--|-' +| .-' '-------------. +| v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || | || orphan!| || | + || | || | || | + |'--------' |'--------' |'--------' + '--------' '--------' '--------' + +remove dir B from threaded linked-list, returning dir B to free blocks +=> + .--------. + .| root |-. + || | | +.-------|| |-' +| |'--------' +| '---|--|-' +| .-' '-. +| v v +| .--------. .--------. +'->| dir A |->| dir C | + || | || | + || | || | + |'--------' |'--------' + '--------' '--------' +``` + +In addition to normal directory tree operations, we can use orphans to evict +blocks in a metadata pair when the block goes bad or exceeds its allocated +erases. If we lose power while evicting a metadata block we may end up with +a situation where the filesystem references the replacement block while the +threaded linked-list still contains the evicted block. We call this a +half-orphan. + +``` + .--------. + .| root |-. + || | | +.-------------|| |-' +| |'--------' +| '--|-|-|-' +| .------' | '-------. +| v v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || | || | || | + || | || | || | + |'--------' |'--------' |'--------' + '--------' '--------' '--------' + +try to write to dir B +=> + .--------. + .| root |-. + || | | +.----------------|| |-' +| |'--------' +| '-|-||-|-' +| .--------' || '-----. +| v |v v +| .--------. .--------. .--------. +'->| dir A |---->| dir B |->| dir C | + || |-. | | || | + || | | | | || | + |'--------' | '--------' |'--------' + '--------' | v '--------' + | .--------. + '->| dir B | + | bad | + | block! | + '--------' + +oh no! bad block detected, allocate replacement +=> + .--------. + .| root |-. + || | | +.----------------|| |-' +| |'--------' +| '-|-||-|-' +| .--------' || '-------. +| v |v v +| .--------. .--------. .--------. +'->| dir A |---->| dir B |--->| dir C | + || |-. | | .->| | + || | | | | | || | + |'--------' | '--------' | |'--------' + '--------' | v | '--------' + | .--------. | + '->| dir B | | + | bad | | + | block! | | + '--------' | + | + .--------. | + | dir B |--' + | | + | | + '--------' + +insert replacement in threaded linked-list, creating a half-orphan +=> + .--------. + .| root |-. + || | | +.----------------|| |-' +| |'--------' +| '-|-||-|-' +| .--------' || '-------. +| v |v v +| .--------. .--------. .--------. +'->| dir A |---->| dir B |--->| dir C | + || |-. | | .->| | + || | | | | | || | + |'--------' | '--------' | |'--------' + '--------' | v | '--------' + | .--------. | + | | dir B | | + | | bad | | + | | block! | | + | '--------' | + | | + | .--------. | + '->| dir B |--' + | half | + | orphan!| + '--------' + +fix reference in parent directory +=> + .--------. + .| root |-. + || | | +.-------------|| |-' +| |'--------' +| '--|-|-|-' +| .------' | '-------. +| v v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || | || | || | + || | || | || | + |'--------' |'--------' |'--------' + '--------' '--------' '--------' +``` + +Finding orphans and half-orphans is expensive, requiring a _O(n²)_ +comparison of every metadata pair with every directory entry. But the tradeoff +is a power resilient filesystem that works with only a bounded amount of RAM. +Fortunately, we only need to check for orphans on the first allocation after +boot, and a read-only littlefs can ignore the threaded linked-list entirely. + +If we only had some sort of global state, then we could also store a flag and +avoid searching for orphans unless we knew we were specifically interrupted +while manipulating the directory tree (foreshadowing!). + +## The move problem + +We have one last challenge: the move problem. Phrasing the problem is simple: + +How do you atomically move a file between two directories? + +In littlefs we can atomically commit to directories, but we can't create +an atomic commit that spans multiple directories. The filesystem must go +through a minimum of two distinct states to complete a move. + +To make matters worse, file moves are a common form of synchronization for +filesystems. As a filesystem designed for power-loss, it's important we get +atomic moves right. + +So what can we do? + +- We definitely can't just let power-loss result in duplicated or lost files. + This could easily break users' code and would only reveal itself in extreme + cases. We were only able to be lazy about the threaded linked-list because + it isn't user facing and we can handle the corner cases internally. + +- Some filesystems propagate COW operations up the tree until a common parent + is found. Unfortunately this interacts poorly with our threaded tree and + brings back the issue of upward propagation of wear. + +- In a previous version of littlefs we tried to solve this problem by going + back and forth between the source and destination, marking and unmarking the + file as moving in order to make the move atomic from the user perspective. + This worked, but not well. Finding failed moves was expensive and required + a unique identifier for each file. + +In the end, solving the move problem required creating a new mechanism for +sharing knowledge between multiple metadata pairs. In littlefs this led to the +introduction of a mechanism called "global state". + +--- + +Global state is a small set of state that can be updated from _any_ metadata +pair. Combining global state with metadata pairs' ability to update multiple +entries in one commit gives us a powerful tool for crafting complex atomic +operations. + +How does global state work? + +Global state exists as a set of deltas that are distributed across the metadata +pairs in the filesystem. The actual global state can be built out of these +deltas by xoring together all of the deltas in the filesystem. + +``` + .--------. .--------. .--------. .--------. .--------. +.| |->| gdelta |->| |->| gdelta |->| gdelta | +|| | || 0x23 | || | || 0xff | || 0xce | +|| | || | || | || | || | +|'--------' |'--------' |'--------' |'--------' |'--------' +'--------' '----|---' '--------' '----|---' '----|---' + v v v + 0x00 --> xor ------------------> xor ------> xor --> gstate 0x12 +``` + +To update the global state from a metadata pair, we take the global state we +know and xor it with both our changes and any existing delta in the metadata +pair. Committing this new delta to the metadata pair commits the changes to +the filesystem's global state. + +``` + .--------. .--------. .--------. .--------. .--------. +.| |->| gdelta |->| |->| gdelta |->| gdelta | +|| | || 0x23 | || | || 0xff | || 0xce | +|| | || | || | || | || | +|'--------' |'--------' |'--------' |'--------' |'--------' +'--------' '----|---' '--------' '--|---|-' '----|---' + v v | v + 0x00 --> xor ----------------> xor -|------> xor --> gstate = 0x12 + | | + | | +change gstate to 0xab --> xor <------------|--------------------------' +=> | v + '------------> xor + | + v + .--------. .--------. .--------. .--------. .--------. +.| |->| gdelta |->| |->| gdelta |->| gdelta | +|| | || 0x23 | || | || 0x46 | || 0xce | +|| | || | || | || | || | +|'--------' |'--------' |'--------' |'--------' |'--------' +'--------' '----|---' '--------' '----|---' '----|---' + v v v + 0x00 --> xor ------------------> xor ------> xor --> gstate = 0xab +``` + +To make this efficient, we always keep a copy of the global state in RAM. We +only need to iterate over our metadata pairs and build the global state when +the filesystem is mounted. + +You may have noticed that global state is very expensive. We keep a copy in +RAM and a delta in an unbounded number of metadata pairs. Even if we reset +the global state to its initial value, we can't easily clean up the deltas on +disk. For this reason, it's very important that we keep the size of global +state bounded and extremely small. But, even with a strict budget, global +state is incredibly valuable. + +--- + +Now we can solve the move problem. We can create global state describing our +move atomically with the creation of the new file, and we can clear this move +state atomically with the removal of the old file. + +``` + .--------. gstate = no move + .| root |-. + || | | +.-------------|| |-' +| |'--------' +| '--|-|-|-' +| .------' | '-------. +| v v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || | || | || | + || | || | || | + |'--------' |'--------' |'--------' + '----|---' '--------' '--------' + v + .--------. + | file D | + | | + | | + '--------' + +begin move, add reference in dir C, change gstate to have move +=> + .--------. gstate = moving file D in dir A (m1) + .| root |-. + || | | +.-------------|| |-' +| |'--------' +| '--|-|-|-' +| .------' | '-------. +| v v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || | || | || gdelta | + || | || | || =m1 | + |'--------' |'--------' |'--------' + '----|---' '--------' '----|---' + | .----------------' + v v + .--------. + | file D | + | | + | | + '--------' + +complete move, remove reference in dir A, change gstate to no move +=> + .--------. gstate = no move (m1^~m1) + .| root |-. + || | | +.-------------|| |-' +| |'--------' +| '--|-|-|-' +| .------' | '-------. +| v v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || gdelta | || | || gdelta | + || =~m1 | || | || =m1 | + |'--------' |'--------' |'--------' + '--------' '--------' '----|---' + v + .--------. + | file D | + | | + | | + '--------' +``` + + +If, after building our global state during mount, we find information +describing an ongoing move, we know we lost power during a move and the file +is duplicated in both the source and destination directories. If this happens, +we can resolve the move using the information in the global state to remove +one of the files. + +``` + .--------. gstate = moving file D in dir A (m1) + .| root |-. ^ + || |------------> xor +.---------------|| |-' ^ +| |'--------' | +| '--|-|-|-' | +| .--------' | '---------. | +| | | | | +| | .----------> xor --------> xor +| v | v ^ v ^ +| .--------. | .--------. | .--------. | +'->| dir A |-|->| dir B |-|->| dir C | | + || |-' || |-' || gdelta |-' + || | || | || =m1 | + |'--------' |'--------' |'--------' + '----|---' '--------' '----|---' + | .---------------------' + v v + .--------. + | file D | + | | + | | + '--------' +``` + +We can also move directories the same way we move files. There is the threaded +linked-list to consider, but leaving the threaded linked-list unchanged works +fine as the order doesn't really matter. + +``` + .--------. gstate = no move (m1^~m1) + .| root |-. + || | | +.-------------|| |-' +| |'--------' +| '--|-|-|-' +| .------' | '-------. +| v v v +| .--------. .--------. .--------. +'->| dir A |->| dir B |->| dir C | + || gdelta | || | || gdelta | + || =~m1 | || | || =m1 | + |'--------' |'--------' |'--------' + '--------' '--------' '----|---' + v + .--------. + | file D | + | | + | | + '--------' + +begin move, add reference in dir C, change gstate to have move +=> + .--------. gstate = moving dir B in root (m1^~m1^m2) + .| root |-. + || | | +.--------------|| |-' +| |'--------' +| '--|-|-|-' +| .-------' | '----------. +| v | v +| .--------. | .--------. +'->| dir A |-. | .->| dir C | + || gdelta | | | | || gdelta | + || =~m1 | | | | || =m1^m2 | + |'--------' | | | |'--------' + '--------' | | | '---|--|-' + | | .-------' | + | v v | v + | .--------. | .--------. + '->| dir B |-' | file D | + || | | | + || | | | + |'--------' '--------' + '--------' + +complete move, remove reference in root, change gstate to no move +=> + .--------. gstate = no move (m1^~m1^m2^~m2) + .| root |-. + || gdelta | | +.-----------|| =~m2 |-' +| |'--------' +| '---|--|-' +| .-----' '-----. +| v v +| .--------. .--------. +'->| dir A |-. .->| dir C | + || gdelta | | | || gdelta | + || =~m1 | | '-|| =m1^m2 |-------. + |'--------' | |'--------' | + '--------' | '---|--|-' | + | .-' '-. | + | v v | + | .--------. .--------. | + '->| dir B |--| file D |-' + || | | | + || | | | + |'--------' '--------' + '--------' +``` + +Global state gives us a powerful tool we can use to solve the move problem. +And the result is surprisingly performant, only needing the minimum number +of states and using the same number of commits as a naive move. Additionally, +global state gives us a bit of persistent state we can use for some other +small improvements. + +## Conclusion + +And that's littlefs, thanks for reading! + + +[wikipedia-flash]: https://en.wikipedia.org/wiki/Flash_memory +[wikipedia-sna]: https://en.wikipedia.org/wiki/Serial_number_arithmetic +[wikipedia-crc]: https://en.wikipedia.org/wiki/Cyclic_redundancy_check +[wikipedia-cow]: https://en.wikipedia.org/wiki/Copy-on-write +[wikipedia-B-tree]: https://en.wikipedia.org/wiki/B-tree +[wikipedia-B+-tree]: https://en.wikipedia.org/wiki/B%2B_tree +[wikipedia-skip-list]: https://en.wikipedia.org/wiki/Skip_list +[wikipedia-ctz]: https://en.wikipedia.org/wiki/Count_trailing_zeros +[wikipedia-ecc]: https://en.wikipedia.org/wiki/Error_correction_code +[wikipedia-hamming-bound]: https://en.wikipedia.org/wiki/Hamming_bound +[wikipedia-dynamic-wear-leveling]: https://en.wikipedia.org/wiki/Wear_leveling#Dynamic_wear_leveling +[wikipedia-static-wear-leveling]: https://en.wikipedia.org/wiki/Wear_leveling#Static_wear_leveling +[wikipedia-ftl]: https://en.wikipedia.org/wiki/Flash_translation_layer + +[oeis]: https://oeis.org +[A001511]: https://oeis.org/A001511 +[A005187]: https://oeis.org/A005187 + +[fat]: https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system +[ext2]: http://e2fsprogs.sourceforge.net/ext2intro.html +[jffs]: https://www.sourceware.org/jffs2/jffs2-html +[yaffs]: https://yaffs.net/documents/how-yaffs-works +[spiffs]: https://github.com/pellepl/spiffs/blob/master/docs/TECH_SPEC +[ext4]: https://ext4.wiki.kernel.org/index.php/Ext4_Design +[ntfs]: https://en.wikipedia.org/wiki/NTFS +[btrfs]: https://btrfs.wiki.kernel.org/index.php/Btrfs_design +[zfs]: https://en.wikipedia.org/wiki/ZFS + +[cow]: https://upload.wikimedia.org/wikipedia/commons/0/0c/Cow_female_black_white.jpg +[elephant]: https://upload.wikimedia.org/wikipedia/commons/3/37/African_Bush_Elephant.jpg +[ram]: https://upload.wikimedia.org/wikipedia/commons/9/97/New_Mexico_Bighorn_Sheep.JPG + +[metadata-formula1]: https://latex.codecogs.com/svg.latex?cost%20%3D%20n%20+%20n%20%5Cfrac%7Bs%7D%7Bd+1%7D +[metadata-formula2]: https://latex.codecogs.com/svg.latex?s%20%3D%20r%20%5Cfrac%7Bsize%7D%7Bn%7D +[metadata-formula3]: https://latex.codecogs.com/svg.latex?d%20%3D%20%281-r%29%20%5Cfrac%7Bsize%7D%7Bn%7D +[metadata-formula4]: https://latex.codecogs.com/svg.latex?cost%20%3D%20n%20+%20n%20%5Cfrac%7Br%5Cfrac%7Bsize%7D%7Bn%7D%7D%7B%281-r%29%5Cfrac%7Bsize%7D%7Bn%7D+1%7D + +[ctz-formula1]: https://latex.codecogs.com/svg.latex?%5Clim_%7Bn%5Cto%5Cinfty%7D%5Cfrac%7B1%7D%7Bn%7D%5Csum_%7Bi%3D0%7D%5E%7Bn%7D%5Cleft%28%5Ctext%7Bctz%7D%28i%29+1%5Cright%29%20%3D%20%5Csum_%7Bi%3D0%7D%5Cfrac%7B1%7D%7B2%5Ei%7D%20%3D%202 +[ctz-formula2]: https://latex.codecogs.com/svg.latex?B%20%3D%20%5Cfrac%7Bw%7D%7B8%7D%5Cleft%5Clceil%5Clog_2%5Cleft%28%5Cfrac%7B2%5Ew%7D%7BB-2%5Cfrac%7Bw%7D%7B8%7D%7D%5Cright%29%5Cright%5Crceil +[ctz-formula3]: https://latex.codecogs.com/svg.latex?N%20%3D%20%5Csum_i%5En%5Cleft%5BB-%5Cfrac%7Bw%7D%7B8%7D%5Cleft%28%5Ctext%7Bctz%7D%28i%29+1%5Cright%29%5Cright%5D +[ctz-formula4]: https://latex.codecogs.com/svg.latex?%5Csum_i%5En%5Cleft%28%5Ctext%7Bctz%7D%28i%29+1%5Cright%29%20%3D%202n-%5Ctext%7Bpopcount%7D%28n%29 +[ctz-formula5]: https://latex.codecogs.com/svg.latex?N%20%3D%20Bn%20-%20%5Cfrac%7Bw%7D%7B8%7D%5Cleft%282n-%5Ctext%7Bpopcount%7D%28n%29%5Cright%29 +[ctz-formula6]: https://latex.codecogs.com/svg.latex?n%20%3D%20%5Cleft%5Clfloor%5Cfrac%7BN-%5Cfrac%7Bw%7D%7B8%7D%5Cleft%28%5Ctext%7Bpopcount%7D%5Cleft%28%5Cfrac%7BN%7D%7BB-2%5Cfrac%7Bw%7D%7B8%7D%7D-1%5Cright%29+2%5Cright%29%7D%7BB-2%5Cfrac%7Bw%7D%7B8%7D%7D%5Cright%5Crfloor +[ctz-formula7]: https://latex.codecogs.com/svg.latex?%5Cmathit%7Boff%7D%20%3D%20N%20-%20%5Cleft%28B-2%5Cfrac%7Bw%7D%7B8%7D%5Cright%29n%20-%20%5Cfrac%7Bw%7D%7B8%7D%5Ctext%7Bpopcount%7D%28n%29 + +[bigB]: https://latex.codecogs.com/svg.latex?B +[d]: https://latex.codecogs.com/svg.latex?d +[m]: https://latex.codecogs.com/svg.latex?m +[bigN]: https://latex.codecogs.com/svg.latex?N +[n]: https://latex.codecogs.com/svg.latex?n +[n']: https://latex.codecogs.com/svg.latex?n%27 +[r]: https://latex.codecogs.com/svg.latex?r +[s]: https://latex.codecogs.com/svg.latex?s +[w]: https://latex.codecogs.com/svg.latex?w +[x]: https://latex.codecogs.com/svg.latex?x + +[metadata-cost-graph]: https://raw.githubusercontent.com/geky/littlefs/gh-images/metadata-cost.svg?sanitize=true +[wear-distribution-graph]: https://raw.githubusercontent.com/geky/littlefs/gh-images/wear-distribution.svg?sanitize=true +[file-cost-graph]: https://raw.githubusercontent.com/geky/littlefs/gh-images/file-cost.svg?sanitize=true diff --git a/features/storage/filesystem/littlefsv2/littlefs/LICENSE.md b/features/storage/filesystem/littlefsv2/littlefs/LICENSE.md new file mode 100644 index 00000000000..ed69bea4743 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/LICENSE.md @@ -0,0 +1,24 @@ +Copyright (c) 2017, Arm Limited. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +- Neither the name of ARM nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/features/storage/filesystem/littlefsv2/littlefs/Makefile b/features/storage/filesystem/littlefsv2/littlefs/Makefile new file mode 100644 index 00000000000..29d2a9981bc --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/Makefile @@ -0,0 +1,69 @@ +TARGET = lfs2.a +ifneq ($(wildcard test.c main.c),) +override TARGET = lfs2 +endif + +CC ?= gcc +AR ?= ar +SIZE ?= size + +SRC += $(wildcard *.c bd/*.c) +OBJ := $(SRC:.c=.o) +DEP := $(SRC:.c=.d) +ASM := $(SRC:.c=.s) + +ifdef DEBUG +override CFLAGS += -O0 -g3 +else +override CFLAGS += -Os +endif +ifdef WORD +override CFLAGS += -m$(WORD) +endif +ifdef TRACE +override CFLAGS += -DLFS2_YES_TRACE +endif +override CFLAGS += -I. +override CFLAGS += -std=c99 -Wall -pedantic +override CFLAGS += -Wextra -Wshadow -Wjump-misses-init -Wundef +# Remove missing-field-initializers because of GCC bug +override CFLAGS += -Wno-missing-field-initializers + +ifdef VERBOSE +override TFLAGS += -v +endif + + +all: $(TARGET) + +asm: $(ASM) + +size: $(OBJ) + $(SIZE) -t $^ + +test: + ./scripts/test.py $(TFLAGS) +.SECONDEXPANSION: +test%: tests/test$$(firstword $$(subst \#, ,%)).toml + ./scripts/test.py $@ $(TFLAGS) + +-include $(DEP) + +lfs2: $(OBJ) + $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@ + +%.a: $(OBJ) + $(AR) rcs $@ $^ + +%.o: %.c + $(CC) -c -MMD $(CFLAGS) $< -o $@ + +%.s: %.c + $(CC) -S $(CFLAGS) $< -o $@ + +clean: + rm -f $(TARGET) + rm -f $(OBJ) + rm -f $(DEP) + rm -f $(ASM) + rm -f tests/*.toml.* diff --git a/features/storage/filesystem/littlefsv2/littlefs/README.md b/features/storage/filesystem/littlefsv2/littlefs/README.md new file mode 100644 index 00000000000..b9cf0bd13a3 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/README.md @@ -0,0 +1,252 @@ +## littlefs + +A little fail-safe filesystem designed for microcontrollers. + +``` + | | | .---._____ + .-----. | | +--|o |---| littlefs | +--| |---| | + '-----' '----------' + | | | +``` + +**Power-loss resilience** - littlefs is designed to handle random power +failures. All file operations have strong copy-on-write guarantees and if +power is lost the filesystem will fall back to the last known good state. + +**Dynamic wear leveling** - littlefs is designed with flash in mind, and +provides wear leveling over dynamic blocks. Additionally, littlefs can +detect bad blocks and work around them. + +**Bounded RAM/ROM** - littlefs is designed to work with a small amount of +memory. RAM usage is strictly bounded, which means RAM consumption does not +change as the filesystem grows. The filesystem contains no unbounded +recursion and dynamic memory is limited to configurable buffers that can be +provided statically. + +## Example + +Here's a simple example that updates a file named `boot_count` every time +main runs. The program can be interrupted at any time without losing track +of how many times it has been booted and without corrupting the filesystem: + +``` c +#include "lfs2.h" + +// variables used by the filesystem +lfs2_t lfs2; +lfs2_file_t file; + +// configuration of the filesystem is provided by this struct +const struct lfs2_config cfg = { + // block device operations + .read = user_provided_block_device_read, + .prog = user_provided_block_device_prog, + .erase = user_provided_block_device_erase, + .sync = user_provided_block_device_sync, + + // block device configuration + .read_size = 16, + .prog_size = 16, + .block_size = 4096, + .block_count = 128, + .cache_size = 16, + .lookahead_size = 16, + .block_cycles = 500, +}; + +// entry point +int main(void) { + // mount the filesystem + int err = lfs2_mount(&lfs2, &cfg); + + // reformat if we can't mount the filesystem + // this should only happen on the first boot + if (err) { + lfs2_format(&lfs2, &cfg); + lfs2_mount(&lfs2, &cfg); + } + + // read current count + uint32_t boot_count = 0; + lfs2_file_open(&lfs2, &file, "boot_count", LFS2_O_RDWR | LFS2_O_CREAT); + lfs2_file_read(&lfs2, &file, &boot_count, sizeof(boot_count)); + + // update boot count + boot_count += 1; + lfs2_file_rewind(&lfs2, &file); + lfs2_file_write(&lfs2, &file, &boot_count, sizeof(boot_count)); + + // remember the storage is not updated until the file is closed successfully + lfs2_file_close(&lfs2, &file); + + // release any resources we were using + lfs2_unmount(&lfs2); + + // print the boot count + printf("boot_count: %d\n", boot_count); +} +``` + +## Usage + +Detailed documentation (or at least as much detail as is currently available) +can be found in the comments in [lfs2.h](lfs2.h). + +littlefs takes in a configuration structure that defines how the filesystem +operates. The configuration struct provides the filesystem with the block +device operations and dimensions, tweakable parameters that tradeoff memory +usage for performance, and optional static buffers if the user wants to avoid +dynamic memory. + +The state of the littlefs is stored in the `lfs2_t` type which is left up +to the user to allocate, allowing multiple filesystems to be in use +simultaneously. With the `lfs2_t` and configuration struct, a user can +format a block device or mount the filesystem. + +Once mounted, the littlefs provides a full set of POSIX-like file and +directory functions, with the deviation that the allocation of filesystem +structures must be provided by the user. + +All POSIX operations, such as remove and rename, are atomic, even in event +of power-loss. Additionally, file updates are not actually committed to +the filesystem until sync or close is called on the file. + +## Other notes + +Littlefs is written in C, and specifically should compile with any compiler +that conforms to the `C99` standard. + +All littlefs calls have the potential to return a negative error code. The +errors can be either one of those found in the `enum lfs2_error` in +[lfs2.h](lfs2.h), or an error returned by the user's block device operations. + +In the configuration struct, the `prog` and `erase` function provided by the +user may return a `LFS2_ERR_CORRUPT` error if the implementation already can +detect corrupt blocks. However, the wear leveling does not depend on the return +code of these functions, instead all data is read back and checked for +integrity. + +If your storage caches writes, make sure that the provided `sync` function +flushes all the data to memory and ensures that the next read fetches the data +from memory, otherwise data integrity can not be guaranteed. If the `write` +function does not perform caching, and therefore each `read` or `write` call +hits the memory, the `sync` function can simply return 0. + +## Design + +At a high level, littlefs is a block based filesystem that uses small logs to +store metadata and larger copy-on-write (COW) structures to store file data. + +In littlefs, these ingredients form a sort of two-layered cake, with the small +logs (called metadata pairs) providing fast updates to metadata anywhere on +storage, while the COW structures store file data compactly and without any +wear amplification cost. + +Both of these data structures are built out of blocks, which are fed by a +common block allocator. By limiting the number of erases allowed on a block +per allocation, the allocator provides dynamic wear leveling over the entire +filesystem. + +``` + root + .--------.--------. + | A'| B'| | + | | |-> | + | | | | + '--------'--------' + .----' '--------------. + A v B v + .--------.--------. .--------.--------. + | C'| D'| | | E'|new| | + | | |-> | | | E'|-> | + | | | | | | | | + '--------'--------' '--------'--------' + .-' '--. | '------------------. + v v .-' v +.--------. .--------. v .--------. +| C | | D | .--------. write | new E | +| | | | | E | ==> | | +| | | | | | | | +'--------' '--------' | | '--------' + '--------' .-' | + .-' '-. .-------------|------' + v v v v + .--------. .--------. .--------. + | F | | G | | new F | + | | | | | | + | | | | | | + '--------' '--------' '--------' +``` + +More details on how littlefs works can be found in [DESIGN.md](DESIGN.md) and +[SPEC.md](SPEC.md). + +- [DESIGN.md](DESIGN.md) - A fully detailed dive into how littlefs works. + I would suggest reading it as the tradeoffs at work are quite interesting. + +- [SPEC.md](SPEC.md) - The on-disk specification of littlefs with all the + nitty-gritty details. May be useful for tooling development. + +## Testing + +The littlefs comes with a test suite designed to run on a PC using the +[emulated block device](emubd/lfs2_emubd.h) found in the emubd directory. +The tests assume a Linux environment and can be started with make: + +``` bash +make test +``` + +## License + +The littlefs is provided under the [BSD-3-Clause] license. See +[LICENSE.md](LICENSE.md) for more information. Contributions to this project +are accepted under the same license. + +Individual files contain the following tag instead of the full license text. + + SPDX-License-Identifier: BSD-3-Clause + +This enables machine processing of license information based on the SPDX +License Identifiers that are here available: http://spdx.org/licenses/ + +## Related projects + +- [littlefs-fuse] - A [FUSE] wrapper for littlefs. The project allows you to + mount littlefs directly on a Linux machine. Can be useful for debugging + littlefs if you have an SD card handy. + +- [littlefs-js] - A javascript wrapper for littlefs. I'm not sure why you would + want this, but it is handy for demos. You can see it in action + [here][littlefs-js-demo]. + +- [mklfs] - A command line tool built by the [Lua RTOS] guys for making + littlefs images from a host PC. Supports Windows, Mac OS, and Linux. + +- [Mbed OS] - The easiest way to get started with littlefs is to jump into Mbed + which already has block device drivers for most forms of embedded storage. + littlefs is available in Mbed OS as the [LittleFileSystem] class. + +- [SPIFFS] - Another excellent embedded filesystem for NOR flash. As a more + traditional logging filesystem with full static wear-leveling, SPIFFS will + likely outperform littlefs on small memories such as the internal flash on + microcontrollers. + +- [Dhara] - An interesting NAND flash translation layer designed for small + MCUs. It offers static wear-leveling and power-resilience with only a fixed + _O(|address|)_ pointer structure stored on each block and in RAM. + + +[BSD-3-Clause]: https://spdx.org/licenses/BSD-3-Clause.html +[littlefs-fuse]: https://github.com/geky/littlefs-fuse +[FUSE]: https://github.com/libfuse/libfuse +[littlefs-js]: https://github.com/geky/littlefs-js +[littlefs-js-demo]:http://littlefs.geky.net/demo.html +[mklfs]: https://github.com/whitecatboard/Lua-RTOS-ESP32/tree/master/components/mklfs/src +[Lua RTOS]: https://github.com/whitecatboard/Lua-RTOS-ESP32 +[Mbed OS]: https://github.com/armmbed/mbed-os +[LittleFileSystem]: https://os.mbed.com/docs/mbed-os/v5.12/apis/littlefilesystem.html +[SPIFFS]: https://github.com/pellepl/spiffs +[Dhara]: https://github.com/dlbeer/dhara diff --git a/features/storage/filesystem/littlefsv2/littlefs/SPEC.md b/features/storage/filesystem/littlefsv2/littlefs/SPEC.md new file mode 100644 index 00000000000..d8c8549103a --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/SPEC.md @@ -0,0 +1,787 @@ +## littlefs technical specification + +This is the technical specification of the little filesystem. This document +covers the technical details of how the littlefs is stored on disk for +introspection and tooling. This document assumes you are familiar with the +design of the littlefs, for more info on how littlefs works check +out [DESIGN.md](DESIGN.md). + +``` + | | | .---._____ + .-----. | | +--|o |---| littlefs | +--| |---| | + '-----' '----------' + | | | +``` + +## Some quick notes + +- littlefs is a block-based filesystem. The disk is divided into an array of + evenly sized blocks that are used as the logical unit of storage. + +- Block pointers are stored in 32 bits, with the special value `0xffffffff` + representing a null block address. + +- In addition to the logical block size (which usually matches the erase + block size), littlefs also uses a program block size and read block size. + These determine the alignment of block device operations, but don't need + to be consistent for portability. + +- By default, all values in littlefs are stored in little-endian byte order. + +## Directories / Metadata pairs + +Metadata pairs form the backbone of littlefs and provide a system for +distributed atomic updates. Even the superblock is stored in a metadata pair. + +As their name suggests, a metadata pair is stored in two blocks, with one block +providing a backup during erase cycles in case power is lost. These two blocks +are not necessarily sequential and may be anywhere on disk, so a "pointer" to a +metadata pair is stored as two block pointers. + +On top of this, each metadata block behaves as an appendable log, containing a +variable number of commits. Commits can be appended to the metadata log in +order to update the metadata without requiring an erase cycles. Note that +successive commits may supersede the metadata in previous commits. Only the +most recent metadata should be considered valid. + +The high-level layout of a metadata block is fairly simple: + +``` + .---------------------------------------. +.-| revision count | entries | \ +| |-------------------+ | | +| | | | +| | | +-- 1st commit +| | | | +| | +-------------------| | +| | | CRC | / +| |-------------------+-------------------| +| | entries | \ +| | | | +| | | +-- 2nd commit +| | +-------------------+--------------| | +| | | CRC | padding | / +| |----+-------------------+--------------| +| | entries | \ +| | | | +| | | +-- 3rd commit +| | +-------------------+---------| | +| | | CRC | | / +| |---------+-------------------+ | +| | unwritten storage | more commits +| | | | +| | | v +| | | +| | | +| '---------------------------------------' +'---------------------------------------' +``` + +Each metadata block contains a 32-bit revision count followed by a number of +commits. Each commit contains a variable number of metadata entries followed +by a 32-bit CRC. + +Note also that entries aren't necessarily word-aligned. This allows us to +store metadata more compactly, however we can only write to addresses that are +aligned to our program block size. This means each commit may have padding for +alignment. + +Metadata block fields: + +1. **Revision count (32-bits)** - Incremented every erase cycle. If both blocks + contain valid commits, only the block with the most recent revision count + should be used. Sequence comparison must be used to avoid issues with + integer overflow. + +2. **CRC (32-bits)** - Detects corruption from power-loss or other write + issues. Uses a CRC-32 with a polynomial of `0x04c11db7` initialized + with `0xffffffff`. + +Entries themselves are stored as a 32-bit tag followed by a variable length +blob of data. But exactly how these tags are stored is a little bit tricky. + +Metadata blocks support both forward and backward iteration. In order to do +this without duplicating the space for each tag, neighboring entries have their +tags XORed together, starting with `0xffffffff`. + +``` + Forward iteration Backward iteration + +.-------------------. 0xffffffff .-------------------. +| revision count | | | revision count | +|-------------------| v |-------------------| +| tag ~A |---> xor -> tag A | tag ~A |---> xor -> 0xffffffff +|-------------------| | |-------------------| ^ +| data A | | | data A | | +| | | | | | +| | | | | | +|-------------------| v |-------------------| | +| tag AxB |---> xor -> tag B | tag AxB |---> xor -> tag A +|-------------------| | |-------------------| ^ +| data B | | | data B | | +| | | | | | +| | | | | | +|-------------------| v |-------------------| | +| tag BxC |---> xor -> tag C | tag BxC |---> xor -> tag B +|-------------------| |-------------------| ^ +| data C | | data C | | +| | | | tag C +| | | | +| | | | +'-------------------' '-------------------' +``` + +One last thing to note before we get into the details around tag encoding. Each +tag contains a valid bit used to indicate if the tag and containing commit is +valid. This valid bit is the first bit found in the tag and the commit and can +be used to tell if we've attempted to write to the remaining space in the +block. + +Here's a more complete example of metadata block containing 4 entries: + +``` + .---------------------------------------. +.-| revision count | tag ~A | \ +| |-------------------+-------------------| | +| | data A | | +| | | | +| |-------------------+-------------------| | +| | tag AxB | data B | <--. | +| |-------------------+ | | | +| | | | +-- 1st commit +| | +-------------------+---------| | | +| | | tag BxC | | <-.| | +| |---------+-------------------+ | || | +| | data C | || | +| | | || | +| |-------------------+-------------------| || | +| | tag CxCRC | CRC | || / +| |-------------------+-------------------| || +| | tag CRCxA' | data A' | || \ +| |-------------------+ | || | +| | | || | +| | +-------------------+----| || +-- 2nd commit +| | | tag CRCxA' | | || | +| |--------------+-------------------+----| || | +| | CRC | padding | || / +| |--------------+----+-------------------| || +| | tag CRCxA'' | data A'' | <---. \ +| |-------------------+ | ||| | +| | | ||| | +| | +-------------------+---------| ||| | +| | | tag A''xD | | < ||| | +| |---------+-------------------+ | |||| +-- 3rd commit +| | data D | |||| | +| | +---------| |||| | +| | | tag Dx| |||| | +| |---------+-------------------+---------| |||| | +| |CRC | CRC | | |||| / +| |---------+-------------------+ | |||| +| | unwritten storage | |||| more commits +| | | |||| | +| | | |||| v +| | | |||| +| | | |||| +| '---------------------------------------' |||| +'---------------------------------------' |||'- most recent A + ||'-- most recent B + |'--- most recent C + '---- most recent D +``` + +## Metadata tags + +So in littlefs, 32-bit tags describe every type of metadata. And this means +_every_ type of metadata, including file entries, directory fields, and +global state. Even the CRCs used to mark the end of commits get their own tag. + +Because of this, the tag format contains some densely packed information. Note +that there are multiple levels of types which break down into more info: + +``` +[---- 32 ----] +[1|-- 11 --|-- 10 --|-- 10 --] + ^. ^ . ^ ^- length + |. | . '------------ id + |. '-----.------------------ type (type3) + '.-----------.------------------ valid bit + [-3-|-- 8 --] + ^ ^- chunk + '------- type (type1) +``` + + +Before we go further, there's one important thing to note. These tags are +**not** stored in little-endian. Tags stored in commits are actually stored +in big-endian (and is the only thing in littlefs stored in big-endian). This +little bit of craziness comes from the fact that the valid bit must be the +first bit in a commit, and when converted to little-endian, the valid bit finds +itself in byte 4. We could restructure the tag to store the valid bit lower, +but, because none of the fields are byte-aligned, this would be more +complicated than just storing the tag in big-endian. + +Another thing to note is that both the tags `0x00000000` and `0xffffffff` are +invalid and can be used for null values. + +Metadata tag fields: + +1. **Valid bit (1-bit)** - Indicates if the tag is valid. + +2. **Type3 (11-bits)** - Type of the tag. This field is broken down further + into a 3-bit abstract type and an 8-bit chunk field. Note that the value + `0x000` is invalid and not assigned a type. + +3. **Type1 (3-bits)** - Abstract type of the tag. Groups the tags into + 8 categories that facilitate bitmasked lookups. + +4. **Chunk (8-bits)** - Chunk field used for various purposes by the different + abstract types. type1+chunk+id form a unique identifier for each tag in the + metadata block. + +5. **Id (10-bits)** - File id associated with the tag. Each file in a metadata + block gets a unique id which is used to associate tags with that file. The + special value `0x3ff` is used for any tags that are not associated with a + file, such as directory and global metadata. + +6. **Length (10-bits)** - Length of the data in bytes. The special value + `0x3ff` indicates that this tag has been deleted. + +## Metadata types + +What follows is an exhaustive list of metadata in littlefs. + +--- +#### `0x401` LFS2_TYPE_CREATE + +Creates a new file with this id. Note that files in a metadata block +don't necessarily need a create tag. All a create does is move over any +files using this id. In this sense a create is similar to insertion into +an imaginary array of files. + +The create and delete tags allow littlefs to keep files in a directory +ordered alphabetically by filename. + +--- +#### `0x4ff` LFS2_TYPE_DELETE + +Deletes the file with this id. An inverse to create, this tag moves over +any files neighboring this id similar to a deletion from an imaginary +array of files. + +--- +#### `0x0xx` LFS2_TYPE_NAME + +Associates the id with a file name and file type. + +The data contains the file name stored as an ASCII string (may be expanded to +UTF8 in the future). + +The chunk field in this tag indicates an 8-bit file type which can be one of +the following. + +Currently, the name tag must precede any other tags associated with the id and +can not be reassigned without deleting the file. + +Layout of the name tag: + +``` + tag data +[-- 32 --][--- variable length ---] +[1| 3| 8 | 10 | 10 ][--- (size * 8) ---] + ^ ^ ^ ^ ^- size ^- file name + | | | '------ id + | | '----------- file type + | '-------------- type1 (0x0) + '----------------- valid bit +``` + +Name fields: + +1. **file type (8-bits)** - Type of the file. + +2. **file name** - File name stored as an ASCII string. + +--- +#### `0x001` LFS2_TYPE_REG + +Initializes the id + name as a regular file. + +How each file is stored depends on its struct tag, which is described below. + +--- +#### `0x002` LFS2_TYPE_DIR + +Initializes the id + name as a directory. + +Directories in littlefs are stored on disk as a linked-list of metadata pairs, +each pair containing any number of files in alphabetical order. A pointer to +the directory is stored in the struct tag, which is described below. + +--- +#### `0x0ff` LFS2_TYPE_SUPERBLOCK + +Initializes the id as a superblock entry. + +The superblock entry is a special entry used to store format-time configuration +and identify the filesystem. + +The name is a bit of a misnomer. While the superblock entry serves the same +purpose as a superblock found in other filesystems, in littlefs the superblock +does not get a dedicated block. Instead, the superblock entry is duplicated +across a linked-list of metadata pairs rooted on the blocks 0 and 1. The last +metadata pair doubles as the root directory of the filesystem. + +``` + .--------. .--------. .--------. .--------. .--------. +.| super |->| super |->| super |->| super |->| file B | +|| block | || block | || block | || block | || file C | +|| | || | || | || file A | || file D | +|'--------' |'--------' |'--------' |'--------' |'--------' +'--------' '--------' '--------' '--------' '--------' + +\----------------+----------------/ \----------+----------/ + superblock pairs root directory +``` + +The filesystem starts with only the root directory. The superblock metadata +pairs grow every time the root pair is compacted in order to prolong the +life of the device exponentially. + +The contents of the superblock entry are stored in a name tag with the +superblock type and an inline-struct tag. The name tag contains the magic +string "littlefs", while the inline-struct tag contains version and +configuration information. + +Layout of the superblock name tag and inline-struct tag: + +``` + tag data +[-- 32 --][-- 32 --|-- 32 --] +[1|- 11 -| 10 | 10 ][--- 64 ---] + ^ ^ ^ ^- size (8) ^- magic string ("littlefs") + | | '------ id (0) + | '------------ type (0x0ff) + '----------------- valid bit + + tag data +[-- 32 --][-- 32 --|-- 32 --|-- 32 --] +[1|- 11 -| 10 | 10 ][-- 32 --|-- 32 --|-- 32 --] + ^ ^ ^ ^ ^- version ^- block size ^- block count + | | | | [-- 32 --|-- 32 --|-- 32 --] + | | | | [-- 32 --|-- 32 --|-- 32 --] + | | | | ^- name max ^- file max ^- attr max + | | | '- size (24) + | | '------ id (0) + | '------------ type (0x201) + '----------------- valid bit +``` + +Superblock fields: + +1. **Magic string (8-bytes)** - Magic string indicating the presence of + littlefs on the device. Must be the string "littlefs". + +2. **Version (32-bits)** - The version of littlefs at format time. The version + is encoded in a 32-bit value with the upper 16-bits containing the major + version, and the lower 16-bits containing the minor version. + + This specification describes version 2.0 (`0x00020000`). + +3. **Block size (32-bits)** - Size of the logical block size used by the + filesystem in bytes. + +4. **Block count (32-bits)** - Number of blocks in the filesystem. + +5. **Name max (32-bits)** - Maximum size of file names in bytes. + +6. **File max (32-bits)** - Maximum size of files in bytes. + +7. **Attr max (32-bits)** - Maximum size of file attributes in bytes. + +The superblock must always be the first entry (id 0) in a metadata pair as well +as be the first entry written to the block. This means that the superblock +entry can be read from a device using offsets alone. + +--- +#### `0x2xx` LFS2_TYPE_STRUCT + +Associates the id with an on-disk data structure. + +The exact layout of the data depends on the data structure type stored in the +chunk field and can be one of the following. + +Any type of struct supersedes all other structs associated with the id. For +example, appending a ctz-struct replaces an inline-struct on the same file. + +--- +#### `0x200` LFS2_TYPE_DIRSTRUCT + +Gives the id a directory data structure. + +Directories in littlefs are stored on disk as a linked-list of metadata pairs, +each pair containing any number of files in alphabetical order. + +``` + | + v + .--------. .--------. .--------. .--------. .--------. .--------. +.| file A |->| file D |->| file G |->| file I |->| file J |->| file M | +|| file B | || file E | || file H | || | || file K | || file N | +|| file C | || file F | || | || | || file L | || | +|'--------' |'--------' |'--------' |'--------' |'--------' |'--------' +'--------' '--------' '--------' '--------' '--------' '--------' +``` + +The dir-struct tag contains only the pointer to the first metadata-pair in the +directory. The directory size is not known without traversing the directory. + +The pointer to the next metadata-pair in the directory is stored in a tail tag, +which is described below. + +Layout of the dir-struct tag: + +``` + tag data +[-- 32 --][-- 32 --|-- 32 --] +[1|- 11 -| 10 | 10 ][--- 64 ---] + ^ ^ ^ ^- size (8) ^- metadata pair + | | '------ id + | '------------ type (0x200) + '----------------- valid bit +``` + +Dir-struct fields: + +1. **Metadata pair (8-bytes)** - Pointer to the first metadata-pair + in the directory. + +--- +#### `0x201` LFS2_TYPE_INLINESTRUCT + +Gives the id an inline data structure. + +Inline structs store small files that can fit in the metadata pair. In this +case, the file data is stored directly in the tag's data area. + +Layout of the inline-struct tag: + +``` + tag data +[-- 32 --][--- variable length ---] +[1|- 11 -| 10 | 10 ][--- (size * 8) ---] + ^ ^ ^ ^- size ^- inline data + | | '------ id + | '------------ type (0x201) + '----------------- valid bit +``` + +Inline-struct fields: + +1. **Inline data** - File data stored directly in the metadata-pair. + +--- +#### `0x202` LFS2_TYPE_CTZSTRUCT + +Gives the id a CTZ skip-list data structure. + +CTZ skip-lists store files that can not fit in the metadata pair. These files +are stored in a skip-list in reverse, with a pointer to the head of the +skip-list. Note that the head of the skip-list and the file size is enough +information to read the file. + +How exactly CTZ skip-lists work is a bit complicated. A full explanation can be +found in the [DESIGN.md](DESIGN.md#ctz-skip-lists). + +A quick summary: For every _n_‍th block where _n_ is divisible by +2‍_ˣ_, that block contains a pointer to block _n_-2‍_ˣ_. +These pointers are stored in increasing order of _x_ in each block of the file +before the actual data. + +``` + | + v +.--------. .--------. .--------. .--------. .--------. .--------. +| A |<-| D |<-| G |<-| J |<-| M |<-| P | +| B |<-| E |--| H |<-| K |--| N | | Q | +| C |<-| F |--| I |--| L |--| O | | | +'--------' '--------' '--------' '--------' '--------' '--------' + block 0 block 1 block 2 block 3 block 4 block 5 + 1 skip 2 skips 1 skip 3 skips 1 skip +``` + +Note that the maximum number of pointers in a block is bounded by the maximum +file size divided by the block size. With 32 bits for file size, this results +in a minimum block size of 104 bytes. + +Layout of the CTZ-struct tag: + +``` + tag data +[-- 32 --][-- 32 --|-- 32 --] +[1|- 11 -| 10 | 10 ][-- 32 --|-- 32 --] + ^ ^ ^ ^ ^ ^- file size + | | | | '-------------------- file head + | | | '- size (8) + | | '------ id + | '------------ type (0x202) + '----------------- valid bit +``` + +CTZ-struct fields: + +1. **File head (32-bits)** - Pointer to the block that is the head of the + file's CTZ skip-list. + +2. **File size (32-bits)** - Size of the file in bytes. + +--- +#### `0x3xx` LFS2_TYPE_USERATTR + +Attaches a user attribute to an id. + +littlefs has a concept of "user attributes". These are small user-provided +attributes that can be used to store things like timestamps, hashes, +permissions, etc. + +Each user attribute is uniquely identified by an 8-bit type which is stored in +the chunk field, and the user attribute itself can be found in the tag's data. + +There are currently no standard user attributes and a portable littlefs +implementation should work with any user attributes missing. + +Layout of the user-attr tag: + +``` + tag data +[-- 32 --][--- variable length ---] +[1| 3| 8 | 10 | 10 ][--- (size * 8) ---] + ^ ^ ^ ^ ^- size ^- attr data + | | | '------ id + | | '----------- attr type + | '-------------- type1 (0x3) + '----------------- valid bit +``` + +User-attr fields: + +1. **Attr type (8-bits)** - Type of the user attributes. + +2. **Attr data** - The data associated with the user attribute. + +--- +#### `0x6xx` LFS2_TYPE_TAIL + +Provides the tail pointer for the metadata pair itself. + +The metadata pair's tail pointer is used in littlefs for a linked-list +containing all metadata pairs. The chunk field contains the type of the tail, +which indicates if the following metadata pair is a part of the directory +(hard-tail) or only used to traverse the filesystem (soft-tail). + +``` + .--------. + .| dir A |-. + ||softtail| | +.--------| |-' +| |'--------' +| '---|--|-' +| .-' '-------------. +| v v +| .--------. .--------. .--------. +'->| dir B |->| dir B |->| dir C | + ||hardtail| ||softtail| || | + || | || | || | + |'--------' |'--------' |'--------' + '--------' '--------' '--------' +``` + +Currently any type supersedes any other preceding tails in the metadata pair, +but this may change if additional metadata pair state is added. + +A note about the metadata pair linked-list: Normally, this linked-list contains +every metadata pair in the filesystem. However, there are some operations that +can cause this linked-list to become out of sync if a power-loss were to occur. +When this happens, littlefs sets the "sync" flag in the global state. How +exactly this flag is stored is described below. + +When the sync flag is set: + +1. The linked-list may contain an orphaned directory that has been removed in + the filesystem. +2. The linked-list may contain a metadata pair with a bad block that has been + replaced in the filesystem. + +If the sync flag is set, the threaded linked-list must be checked for these +errors before it can be used reliably. Note that the threaded linked-list can +be ignored if littlefs is mounted read-only. + +Layout of the tail tag: + +``` + tag data +[-- 32 --][-- 32 --|-- 32 --] +[1| 3| 8 | 10 | 10 ][--- 64 ---] + ^ ^ ^ ^ ^- size (8) ^- metadata pair + | | | '------ id + | | '---------- tail type + | '------------- type1 (0x6) + '---------------- valid bit +``` + +Tail fields: + +1. **Tail type (8-bits)** - Type of the tail pointer. + +2. **Metadata pair (8-bytes)** - Pointer to the next metadata-pair. + +--- +#### `0x600` LFS2_TYPE_SOFTTAIL + +Provides a tail pointer that points to the next metadata pair in the +filesystem. + +In this case, the next metadata pair is not a part of our current directory +and should only be followed when traversing the entire filesystem. + +--- +#### `0x601` LFS2_TYPE_HARDTAIL + +Provides a tail pointer that points to the next metadata pair in the +directory. + +In this case, the next metadata pair belongs to the current directory. Note +that because directories in littlefs are sorted alphabetically, the next +metadata pair should only contain filenames greater than any filename in the +current pair. + +--- +#### `0x7xx` LFS2_TYPE_GSTATE + +Provides delta bits for global state entries. + +littlefs has a concept of "global state". This is a small set of state that +can be updated by a commit to _any_ metadata pair in the filesystem. + +The way this works is that the global state is stored as a set of deltas +distributed across the filesystem such that the global state can be found by +the xor-sum of these deltas. + +``` + .--------. .--------. .--------. .--------. .--------. +.| |->| gdelta |->| |->| gdelta |->| gdelta | +|| | || 0x23 | || | || 0xff | || 0xce | +|| | || | || | || | || | +|'--------' |'--------' |'--------' |'--------' |'--------' +'--------' '----|---' '--------' '----|---' '----|---' + v v v + 0x00 --> xor ------------------> xor ------> xor --> gstate = 0x12 +``` + +Note that storing globals this way is very expensive in terms of storage usage, +so any global state should be kept very small. + +The size and format of each piece of global state depends on the type, which +is stored in the chunk field. Currently, the only global state is move state, +which is outlined below. + +--- +#### `0x7ff` LFS2_TYPE_MOVESTATE + +Provides delta bits for the global move state. + +The move state in littlefs is used to store info about operations that could +cause to filesystem to go out of sync if the power is lost. The operations +where this could occur is moves of files between metadata pairs and any +operation that changes metadata pairs on the threaded linked-list. + +In the case of moves, the move state contains a tag + metadata pair describing +the source of the ongoing move. If this tag is non-zero, that means that power +was lost during a move, and the file exists in two different locations. If this +happens, the source of the move should be considered deleted, and the move +should be completed (the source should be deleted) before any other write +operations to the filesystem. + +In the case of operations to the threaded linked-list, a single "sync" bit is +used to indicate that a modification is ongoing. If this sync flag is set, the +threaded linked-list will need to be checked for errors before it can be used +reliably. The exact cases to check for are described above in the tail tag. + +Layout of the move state: + +``` + tag data +[-- 32 --][-- 32 --|-- 32 --|-- 32 --] +[1|- 11 -| 10 | 10 ][1|- 11 -| 10 | 10 |--- 64 ---] + ^ ^ ^ ^ ^ ^ ^ ^- padding (0) ^- metadata pair + | | | | | | '------ move id + | | | | | '------------ move type + | | | | '----------------- sync bit + | | | | + | | | '- size (12) + | | '------ id (0x3ff) + | '------------ type (0x7ff) + '----------------- valid bit +``` + +Move state fields: + +1. **Sync bit (1-bit)** - Indicates if the metadata pair threaded linked-list + is in-sync. If set, the threaded linked-list should be checked for errors. + +2. **Move type (11-bits)** - Type of move being performed. Must be either + `0x000`, indicating no move, or `0x4ff` indicating the source file should + be deleted. + +3. **Move id (10-bits)** - The file id being moved. + +4. **Metadata pair (8-bytes)** - Pointer to the metadata-pair containing + the move. + +--- +#### `0x5xx` LFS2_TYPE_CRC + +Last but not least, the CRC tag marks the end of a commit and provides a +checksum for any commits to the metadata block. + +The first 32-bits of the data contain a CRC-32 with a polynomial of +`0x04c11db7` initialized with `0xffffffff`. This CRC provides a checksum for +all metadata since the previous CRC tag, including the CRC tag itself. For +the first commit, this includes the revision count for the metadata block. + +However, the size of the data is not limited to 32-bits. The data field may +larger to pad the commit to the next program-aligned boundary. + +In addition, the CRC tag's chunk field contains a set of flags which can +change the behaviour of commits. Currently the only flag in use is the lowest +bit, which determines the expected state of the valid bit for any following +tags. This is used to guarantee that unwritten storage in a metadata block +will be detected as invalid. + +Layout of the CRC tag: + +``` + tag data +[-- 32 --][-- 32 --|--- variable length ---] +[1| 3| 8 | 10 | 10 ][-- 32 --|--- (size * 8 - 32) ---] + ^ ^ ^ ^ ^ ^- crc ^- padding + | | | | '- size + | | | '------ id (0x3ff) + | | '----------- valid state + | '-------------- type1 (0x5) + '----------------- valid bit +``` + +CRC fields: + +1. **Valid state (1-bit)** - Indicates the expected value of the valid bit for + any tags in the next commit. + +2. **CRC (32-bits)** - CRC-32 with a polynomial of `0x04c11db7` initialized + with `0xffffffff`. + +3. **Padding** - Padding to the next program-aligned boundary. No guarantees + are made about the contents. + +--- diff --git a/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_filebd.c b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_filebd.c new file mode 100644 index 00000000000..753f5d45ec6 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_filebd.c @@ -0,0 +1,205 @@ +/* + * Block device emulated in a file + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "bd/lfs2_filebd.h" + +#include +#include +#include + +int lfs2_filebd_createcfg(const struct lfs2_config *cfg, const char *path, + const struct lfs2_filebd_config *bdcfg) { + LFS2_FILEBD_TRACE("lfs2_filebd_createcfg(%p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " + "\"%s\", " + "%p {.erase_value=%"PRId32"})", + (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, + path, (void*)bdcfg, bdcfg->erase_value); + lfs2_filebd_t *bd = cfg->context; + bd->cfg = bdcfg; + + // open file + bd->fd = open(path, O_RDWR | O_CREAT, 0666); + if (bd->fd < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_createcfg -> %d", err); + return err; + } + + LFS2_FILEBD_TRACE("lfs2_filebd_createcfg -> %d", 0); + return 0; +} + +int lfs2_filebd_create(const struct lfs2_config *cfg, const char *path) { + LFS2_FILEBD_TRACE("lfs2_filebd_create(%p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " + "\"%s\")", + (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, + path); + static const struct lfs2_filebd_config defaults = {.erase_value=-1}; + int err = lfs2_filebd_createcfg(cfg, path, &defaults); + LFS2_FILEBD_TRACE("lfs2_filebd_create -> %d", err); + return err; +} + +int lfs2_filebd_destroy(const struct lfs2_config *cfg) { + LFS2_FILEBD_TRACE("lfs2_filebd_destroy(%p)", (void*)cfg); + lfs2_filebd_t *bd = cfg->context; + int err = close(bd->fd); + if (err < 0) { + err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_destroy -> %d", err); + return err; + } + LFS2_FILEBD_TRACE("lfs2_filebd_destroy -> %d", 0); + return 0; +} + +int lfs2_filebd_read(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size) { + LFS2_FILEBD_TRACE("lfs2_filebd_read(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs2_filebd_t *bd = cfg->context; + + // check if read is valid + LFS2_ASSERT(off % cfg->read_size == 0); + LFS2_ASSERT(size % cfg->read_size == 0); + LFS2_ASSERT(block < cfg->block_count); + + // zero for reproducability (in case file is truncated) + if (bd->cfg->erase_value != -1) { + memset(buffer, bd->cfg->erase_value, size); + } + + // read + off_t res1 = lseek(bd->fd, + (off_t)block*cfg->block_size + (off_t)off, SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_read -> %d", err); + return err; + } + + ssize_t res2 = read(bd->fd, buffer, size); + if (res2 < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_read -> %d", err); + return err; + } + + LFS2_FILEBD_TRACE("lfs2_filebd_read -> %d", 0); + return 0; +} + +int lfs2_filebd_prog(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_size_t size) { + LFS2_FILEBD_TRACE("lfs2_filebd_prog(%p, 0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs2_filebd_t *bd = cfg->context; + + // check if write is valid + LFS2_ASSERT(off % cfg->prog_size == 0); + LFS2_ASSERT(size % cfg->prog_size == 0); + LFS2_ASSERT(block < cfg->block_count); + + // check that data was erased? only needed for testing + if (bd->cfg->erase_value != -1) { + off_t res1 = lseek(bd->fd, + (off_t)block*cfg->block_size + (off_t)off, SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_prog -> %d", err); + return err; + } + + for (lfs2_off_t i = 0; i < size; i++) { + uint8_t c; + ssize_t res2 = read(bd->fd, &c, 1); + if (res2 < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_prog -> %d", err); + return err; + } + + LFS2_ASSERT(c == bd->cfg->erase_value); + } + } + + // program data + off_t res1 = lseek(bd->fd, + (off_t)block*cfg->block_size + (off_t)off, SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_prog -> %d", err); + return err; + } + + ssize_t res2 = write(bd->fd, buffer, size); + if (res2 < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_prog -> %d", err); + return err; + } + + LFS2_FILEBD_TRACE("lfs2_filebd_prog -> %d", 0); + return 0; +} + +int lfs2_filebd_erase(const struct lfs2_config *cfg, lfs2_block_t block) { + LFS2_FILEBD_TRACE("lfs2_filebd_erase(%p, 0x%"PRIx32")", (void*)cfg, block); + lfs2_filebd_t *bd = cfg->context; + + // check if erase is valid + LFS2_ASSERT(block < cfg->block_count); + + // erase, only needed for testing + if (bd->cfg->erase_value != -1) { + off_t res1 = lseek(bd->fd, (off_t)block*cfg->block_size, SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_erase -> %d", err); + return err; + } + + for (lfs2_off_t i = 0; i < cfg->block_size; i++) { + ssize_t res2 = write(bd->fd, &(uint8_t){bd->cfg->erase_value}, 1); + if (res2 < 0) { + int err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_erase -> %d", err); + return err; + } + } + } + + LFS2_FILEBD_TRACE("lfs2_filebd_erase -> %d", 0); + return 0; +} + +int lfs2_filebd_sync(const struct lfs2_config *cfg) { + LFS2_FILEBD_TRACE("lfs2_filebd_sync(%p)", (void*)cfg); + // file sync + lfs2_filebd_t *bd = cfg->context; + int err = fsync(bd->fd); + if (err) { + err = -errno; + LFS2_FILEBD_TRACE("lfs2_filebd_sync -> %d", 0); + return err; + } + + LFS2_FILEBD_TRACE("lfs2_filebd_sync -> %d", 0); + return 0; +} diff --git a/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_filebd.h b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_filebd.h new file mode 100644 index 00000000000..83223e18120 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_filebd.h @@ -0,0 +1,73 @@ +/* + * Block device emulated in a file + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS2_FILEBD_H +#define LFS2_FILEBD_H + +#include "lfs2.h" +#include "lfs2_util.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + +// Block device specific tracing +#ifdef LFS2_FILEBD_YES_TRACE +#define LFS2_FILEBD_TRACE(...) LFS2_TRACE(__VA_ARGS__) +#else +#define LFS2_FILEBD_TRACE(...) +#endif + +// filebd config (optional) +struct lfs2_filebd_config { + // 8-bit erase value to use for simulating erases. -1 does not simulate + // erases, which can speed up testing by avoiding all the extra block-device + // operations to store the erase value. + int32_t erase_value; +}; + +// filebd state +typedef struct lfs2_filebd { + int fd; + const struct lfs2_filebd_config *cfg; +} lfs2_filebd_t; + + +// Create a file block device using the geometry in lfs2_config +int lfs2_filebd_create(const struct lfs2_config *cfg, const char *path); +int lfs2_filebd_createcfg(const struct lfs2_config *cfg, const char *path, + const struct lfs2_filebd_config *bdcfg); + +// Clean up memory associated with block device +int lfs2_filebd_destroy(const struct lfs2_config *cfg); + +// Read a block +int lfs2_filebd_read(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size); + +// Program a block +// +// The block must have previously been erased. +int lfs2_filebd_prog(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_size_t size); + +// Erase a block +// +// A block must be erased before being programmed. The +// state of an erased block is undefined. +int lfs2_filebd_erase(const struct lfs2_config *cfg, lfs2_block_t block); + +// Sync the block device +int lfs2_filebd_sync(const struct lfs2_config *cfg); + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_rambd.c b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_rambd.c new file mode 100644 index 00000000000..2e693f2de20 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_rambd.c @@ -0,0 +1,140 @@ +/* + * Block device emulated in RAM + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "bd/lfs2_rambd.h" + +int lfs2_rambd_createcfg(const struct lfs2_config *cfg, + const struct lfs2_rambd_config *bdcfg) { + LFS2_RAMBD_TRACE("lfs2_rambd_createcfg(%p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " + "%p {.erase_value=%"PRId32", .buffer=%p})", + (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, + (void*)bdcfg, bdcfg->erase_value, bdcfg->buffer); + lfs2_rambd_t *bd = cfg->context; + bd->cfg = bdcfg; + + // allocate buffer? + if (bd->cfg->buffer) { + bd->buffer = bd->cfg->buffer; + } else { + bd->buffer = lfs2_malloc(cfg->block_size * cfg->block_count); + if (!bd->buffer) { + LFS2_RAMBD_TRACE("lfs2_rambd_createcfg -> %d", LFS2_ERR_NOMEM); + return LFS2_ERR_NOMEM; + } + } + + // zero for reproducability? + if (bd->cfg->erase_value != -1) { + memset(bd->buffer, bd->cfg->erase_value, + cfg->block_size * cfg->block_count); + } + + LFS2_RAMBD_TRACE("lfs2_rambd_createcfg -> %d", 0); + return 0; +} + +int lfs2_rambd_create(const struct lfs2_config *cfg) { + LFS2_RAMBD_TRACE("lfs2_rambd_create(%p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32"})", + (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); + static const struct lfs2_rambd_config defaults = {.erase_value=-1}; + int err = lfs2_rambd_createcfg(cfg, &defaults); + LFS2_RAMBD_TRACE("lfs2_rambd_create -> %d", err); + return err; +} + +int lfs2_rambd_destroy(const struct lfs2_config *cfg) { + LFS2_RAMBD_TRACE("lfs2_rambd_destroy(%p)", (void*)cfg); + // clean up memory + lfs2_rambd_t *bd = cfg->context; + if (!bd->cfg->buffer) { + lfs2_free(bd->buffer); + } + LFS2_RAMBD_TRACE("lfs2_rambd_destroy -> %d", 0); + return 0; +} + +int lfs2_rambd_read(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size) { + LFS2_RAMBD_TRACE("lfs2_rambd_read(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs2_rambd_t *bd = cfg->context; + + // check if read is valid + LFS2_ASSERT(off % cfg->read_size == 0); + LFS2_ASSERT(size % cfg->read_size == 0); + LFS2_ASSERT(block < cfg->block_count); + + // read data + memcpy(buffer, &bd->buffer[block*cfg->block_size + off], size); + + LFS2_RAMBD_TRACE("lfs2_rambd_read -> %d", 0); + return 0; +} + +int lfs2_rambd_prog(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_size_t size) { + LFS2_RAMBD_TRACE("lfs2_rambd_prog(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs2_rambd_t *bd = cfg->context; + + // check if write is valid + LFS2_ASSERT(off % cfg->prog_size == 0); + LFS2_ASSERT(size % cfg->prog_size == 0); + LFS2_ASSERT(block < cfg->block_count); + + // check that data was erased? only needed for testing + if (bd->cfg->erase_value != -1) { + for (lfs2_off_t i = 0; i < size; i++) { + LFS2_ASSERT(bd->buffer[block*cfg->block_size + off + i] == + bd->cfg->erase_value); + } + } + + // program data + memcpy(&bd->buffer[block*cfg->block_size + off], buffer, size); + + LFS2_RAMBD_TRACE("lfs2_rambd_prog -> %d", 0); + return 0; +} + +int lfs2_rambd_erase(const struct lfs2_config *cfg, lfs2_block_t block) { + LFS2_RAMBD_TRACE("lfs2_rambd_erase(%p, 0x%"PRIx32")", (void*)cfg, block); + lfs2_rambd_t *bd = cfg->context; + + // check if erase is valid + LFS2_ASSERT(block < cfg->block_count); + + // erase, only needed for testing + if (bd->cfg->erase_value != -1) { + memset(&bd->buffer[block*cfg->block_size], + bd->cfg->erase_value, cfg->block_size); + } + + LFS2_RAMBD_TRACE("lfs2_rambd_erase -> %d", 0); + return 0; +} + +int lfs2_rambd_sync(const struct lfs2_config *cfg) { + LFS2_RAMBD_TRACE("lfs2_rambd_sync(%p)", (void*)cfg); + // sync does nothing because we aren't backed by anything real + (void)cfg; + LFS2_RAMBD_TRACE("lfs2_rambd_sync -> %d", 0); + return 0; +} diff --git a/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_rambd.h b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_rambd.h new file mode 100644 index 00000000000..20e460e7712 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_rambd.h @@ -0,0 +1,75 @@ +/* + * Block device emulated in RAM + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS2_RAMBD_H +#define LFS2_RAMBD_H + +#include "lfs2.h" +#include "lfs2_util.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + +// Block device specific tracing +#ifdef LFS2_RAMBD_YES_TRACE +#define LFS2_RAMBD_TRACE(...) LFS2_TRACE(__VA_ARGS__) +#else +#define LFS2_RAMBD_TRACE(...) +#endif + +// rambd config (optional) +struct lfs2_rambd_config { + // 8-bit erase value to simulate erasing with. -1 indicates no erase + // occurs, which is still a valid block device + int32_t erase_value; + + // Optional statically allocated buffer for the block device. + void *buffer; +}; + +// rambd state +typedef struct lfs2_rambd { + uint8_t *buffer; + const struct lfs2_rambd_config *cfg; +} lfs2_rambd_t; + + +// Create a RAM block device using the geometry in lfs2_config +int lfs2_rambd_create(const struct lfs2_config *cfg); +int lfs2_rambd_createcfg(const struct lfs2_config *cfg, + const struct lfs2_rambd_config *bdcfg); + +// Clean up memory associated with block device +int lfs2_rambd_destroy(const struct lfs2_config *cfg); + +// Read a block +int lfs2_rambd_read(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size); + +// Program a block +// +// The block must have previously been erased. +int lfs2_rambd_prog(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_size_t size); + +// Erase a block +// +// A block must be erased before being programmed. The +// state of an erased block is undefined. +int lfs2_rambd_erase(const struct lfs2_config *cfg, lfs2_block_t block); + +// Sync the block device +int lfs2_rambd_sync(const struct lfs2_config *cfg); + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_testbd.c b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_testbd.c new file mode 100644 index 00000000000..60e9bd1b246 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_testbd.c @@ -0,0 +1,302 @@ +/* + * Testing block device, wraps filebd and rambd while providing a bunch + * of hooks for testing littlefs in various conditions. + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "bd/lfs2_testbd.h" + +#include + + +int lfs2_testbd_createcfg(const struct lfs2_config *cfg, const char *path, + const struct lfs2_testbd_config *bdcfg) { + LFS2_TESTBD_TRACE("lfs2_testbd_createcfg(%p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " + "\"%s\", " + "%p {.erase_value=%"PRId32", .erase_cycles=%"PRIu32", " + ".badblock_behavior=%"PRIu8", .power_cycles=%"PRIu32", " + ".buffer=%p, .wear_buffer=%p})", + (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, + path, (void*)bdcfg, bdcfg->erase_value, bdcfg->erase_cycles, + bdcfg->badblock_behavior, bdcfg->power_cycles, + bdcfg->buffer, bdcfg->wear_buffer); + lfs2_testbd_t *bd = cfg->context; + bd->cfg = bdcfg; + + // setup testing things + bd->persist = path; + bd->power_cycles = bd->cfg->power_cycles; + + if (bd->cfg->erase_cycles) { + if (bd->cfg->wear_buffer) { + bd->wear = bd->cfg->wear_buffer; + } else { + bd->wear = lfs2_malloc(sizeof(lfs2_testbd_wear_t)*cfg->block_count); + if (!bd->wear) { + LFS2_TESTBD_TRACE("lfs2_testbd_createcfg -> %d", LFS2_ERR_NOMEM); + return LFS2_ERR_NOMEM; + } + } + + memset(bd->wear, 0, sizeof(lfs2_testbd_wear_t) * cfg->block_count); + } + + // create underlying block device + if (bd->persist) { + bd->u.file.cfg = (struct lfs2_filebd_config){ + .erase_value = bd->cfg->erase_value, + }; + int err = lfs2_filebd_createcfg(cfg, path, &bd->u.file.cfg); + LFS2_TESTBD_TRACE("lfs2_testbd_createcfg -> %d", err); + return err; + } else { + bd->u.ram.cfg = (struct lfs2_rambd_config){ + .erase_value = bd->cfg->erase_value, + .buffer = bd->cfg->buffer, + }; + int err = lfs2_rambd_createcfg(cfg, &bd->u.ram.cfg); + LFS2_TESTBD_TRACE("lfs2_testbd_createcfg -> %d", err); + return err; + } +} + +int lfs2_testbd_create(const struct lfs2_config *cfg, const char *path) { + LFS2_TESTBD_TRACE("lfs2_testbd_create(%p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " + "\"%s\")", + (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, + path); + static const struct lfs2_testbd_config defaults = {.erase_value=-1}; + int err = lfs2_testbd_createcfg(cfg, path, &defaults); + LFS2_TESTBD_TRACE("lfs2_testbd_create -> %d", err); + return err; +} + +int lfs2_testbd_destroy(const struct lfs2_config *cfg) { + LFS2_TESTBD_TRACE("lfs2_testbd_destroy(%p)", (void*)cfg); + lfs2_testbd_t *bd = cfg->context; + if (bd->cfg->erase_cycles && !bd->cfg->wear_buffer) { + lfs2_free(bd->wear); + } + + if (bd->persist) { + int err = lfs2_filebd_destroy(cfg); + LFS2_TESTBD_TRACE("lfs2_testbd_destroy -> %d", err); + return err; + } else { + int err = lfs2_rambd_destroy(cfg); + LFS2_TESTBD_TRACE("lfs2_testbd_destroy -> %d", err); + return err; + } +} + +/// Internal mapping to block devices /// +static int lfs2_testbd_rawread(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size) { + lfs2_testbd_t *bd = cfg->context; + if (bd->persist) { + return lfs2_filebd_read(cfg, block, off, buffer, size); + } else { + return lfs2_rambd_read(cfg, block, off, buffer, size); + } +} + +static int lfs2_testbd_rawprog(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_size_t size) { + lfs2_testbd_t *bd = cfg->context; + if (bd->persist) { + return lfs2_filebd_prog(cfg, block, off, buffer, size); + } else { + return lfs2_rambd_prog(cfg, block, off, buffer, size); + } +} + +static int lfs2_testbd_rawerase(const struct lfs2_config *cfg, + lfs2_block_t block) { + lfs2_testbd_t *bd = cfg->context; + if (bd->persist) { + return lfs2_filebd_erase(cfg, block); + } else { + return lfs2_rambd_erase(cfg, block); + } +} + +static int lfs2_testbd_rawsync(const struct lfs2_config *cfg) { + lfs2_testbd_t *bd = cfg->context; + if (bd->persist) { + return lfs2_filebd_sync(cfg); + } else { + return lfs2_rambd_sync(cfg); + } +} + +/// block device API /// +int lfs2_testbd_read(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size) { + LFS2_TESTBD_TRACE("lfs2_testbd_read(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs2_testbd_t *bd = cfg->context; + + // check if read is valid + LFS2_ASSERT(off % cfg->read_size == 0); + LFS2_ASSERT(size % cfg->read_size == 0); + LFS2_ASSERT(block < cfg->block_count); + + // block bad? + if (bd->cfg->erase_cycles && bd->wear[block] >= bd->cfg->erase_cycles && + bd->cfg->badblock_behavior == LFS2_TESTBD_BADBLOCK_READERROR) { + LFS2_TESTBD_TRACE("lfs2_testbd_read -> %d", LFS2_ERR_CORRUPT); + return LFS2_ERR_CORRUPT; + } + + // read + int err = lfs2_testbd_rawread(cfg, block, off, buffer, size); + LFS2_TESTBD_TRACE("lfs2_testbd_read -> %d", err); + return err; +} + +int lfs2_testbd_prog(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_size_t size) { + LFS2_TESTBD_TRACE("lfs2_testbd_prog(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs2_testbd_t *bd = cfg->context; + + // check if write is valid + LFS2_ASSERT(off % cfg->prog_size == 0); + LFS2_ASSERT(size % cfg->prog_size == 0); + LFS2_ASSERT(block < cfg->block_count); + + // block bad? + if (bd->cfg->erase_cycles && bd->wear[block] >= bd->cfg->erase_cycles) { + if (bd->cfg->badblock_behavior == + LFS2_TESTBD_BADBLOCK_PROGERROR) { + LFS2_TESTBD_TRACE("lfs2_testbd_prog -> %d", LFS2_ERR_CORRUPT); + return LFS2_ERR_CORRUPT; + } else if (bd->cfg->badblock_behavior == + LFS2_TESTBD_BADBLOCK_PROGNOOP || + bd->cfg->badblock_behavior == + LFS2_TESTBD_BADBLOCK_ERASENOOP) { + LFS2_TESTBD_TRACE("lfs2_testbd_prog -> %d", 0); + return 0; + } + } + + // prog + int err = lfs2_testbd_rawprog(cfg, block, off, buffer, size); + if (err) { + LFS2_TESTBD_TRACE("lfs2_testbd_prog -> %d", err); + return err; + } + + // lose power? + if (bd->power_cycles > 0) { + bd->power_cycles -= 1; + if (bd->power_cycles == 0) { + // sync to make sure we persist the last changes + assert(lfs2_testbd_rawsync(cfg) == 0); + // simulate power loss + exit(33); + } + } + + LFS2_TESTBD_TRACE("lfs2_testbd_prog -> %d", 0); + return 0; +} + +int lfs2_testbd_erase(const struct lfs2_config *cfg, lfs2_block_t block) { + LFS2_TESTBD_TRACE("lfs2_testbd_erase(%p, 0x%"PRIx32")", (void*)cfg, block); + lfs2_testbd_t *bd = cfg->context; + + // check if erase is valid + LFS2_ASSERT(block < cfg->block_count); + + // block bad? + if (bd->cfg->erase_cycles) { + if (bd->wear[block] >= bd->cfg->erase_cycles) { + if (bd->cfg->badblock_behavior == + LFS2_TESTBD_BADBLOCK_ERASEERROR) { + LFS2_TESTBD_TRACE("lfs2_testbd_erase -> %d", LFS2_ERR_CORRUPT); + return LFS2_ERR_CORRUPT; + } else if (bd->cfg->badblock_behavior == + LFS2_TESTBD_BADBLOCK_ERASENOOP) { + LFS2_TESTBD_TRACE("lfs2_testbd_erase -> %d", 0); + return 0; + } + } else { + // mark wear + bd->wear[block] += 1; + } + } + + // erase + int err = lfs2_testbd_rawerase(cfg, block); + if (err) { + LFS2_TESTBD_TRACE("lfs2_testbd_erase -> %d", err); + return err; + } + + // lose power? + if (bd->power_cycles > 0) { + bd->power_cycles -= 1; + if (bd->power_cycles == 0) { + // sync to make sure we persist the last changes + assert(lfs2_testbd_rawsync(cfg) == 0); + // simulate power loss + exit(33); + } + } + + LFS2_TESTBD_TRACE("lfs2_testbd_prog -> %d", 0); + return 0; +} + +int lfs2_testbd_sync(const struct lfs2_config *cfg) { + LFS2_TESTBD_TRACE("lfs2_testbd_sync(%p)", (void*)cfg); + int err = lfs2_testbd_rawsync(cfg); + LFS2_TESTBD_TRACE("lfs2_testbd_sync -> %d", err); + return err; +} + + +/// simulated wear operations /// +lfs2_testbd_swear_t lfs2_testbd_getwear(const struct lfs2_config *cfg, + lfs2_block_t block) { + LFS2_TESTBD_TRACE("lfs2_testbd_getwear(%p, %"PRIu32")", (void*)cfg, block); + lfs2_testbd_t *bd = cfg->context; + + // check if block is valid + LFS2_ASSERT(bd->cfg->erase_cycles); + LFS2_ASSERT(block < cfg->block_count); + + LFS2_TESTBD_TRACE("lfs2_testbd_getwear -> %"PRIu32, bd->wear[block]); + return bd->wear[block]; +} + +int lfs2_testbd_setwear(const struct lfs2_config *cfg, + lfs2_block_t block, lfs2_testbd_wear_t wear) { + LFS2_TESTBD_TRACE("lfs2_testbd_setwear(%p, %"PRIu32")", (void*)cfg, block); + lfs2_testbd_t *bd = cfg->context; + + // check if block is valid + LFS2_ASSERT(bd->cfg->erase_cycles); + LFS2_ASSERT(block < cfg->block_count); + + bd->wear[block] = wear; + + LFS2_TESTBD_TRACE("lfs2_testbd_setwear -> %d", 0); + return 0; +} diff --git a/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_testbd.h b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_testbd.h new file mode 100644 index 00000000000..32901510685 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/bd/lfs2_testbd.h @@ -0,0 +1,141 @@ +/* + * Testing block device, wraps filebd and rambd while providing a bunch + * of hooks for testing littlefs in various conditions. + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS2_TESTBD_H +#define LFS2_TESTBD_H + +#include "lfs2.h" +#include "lfs2_util.h" +#include "bd/lfs2_rambd.h" +#include "bd/lfs2_filebd.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + +// Block device specific tracing +#ifdef LFS2_TESTBD_YES_TRACE +#define LFS2_TESTBD_TRACE(...) LFS2_TRACE(__VA_ARGS__) +#else +#define LFS2_TESTBD_TRACE(...) +#endif + +// Mode determining how "bad blocks" behave during testing. This simulates +// some real-world circumstances such as progs not sticking (prog-noop), +// a readonly disk (erase-noop), and ECC failures (read-error). +// +// Not that read-noop is not allowed. Read _must_ return a consistent (but +// may be arbitrary) value on every read. +enum lfs2_testbd_badblock_behavior { + LFS2_TESTBD_BADBLOCK_PROGERROR, + LFS2_TESTBD_BADBLOCK_ERASEERROR, + LFS2_TESTBD_BADBLOCK_READERROR, + LFS2_TESTBD_BADBLOCK_PROGNOOP, + LFS2_TESTBD_BADBLOCK_ERASENOOP, +}; + +// Type for measuring wear +typedef uint32_t lfs2_testbd_wear_t; +typedef int32_t lfs2_testbd_swear_t; + +// testbd config, this is required for testing +struct lfs2_testbd_config { + // 8-bit erase value to use for simulating erases. -1 does not simulate + // erases, which can speed up testing by avoiding all the extra block-device + // operations to store the erase value. + int32_t erase_value; + + // Number of erase cycles before a block becomes "bad". The exact behavior + // of bad blocks is controlled by the badblock_mode. + uint32_t erase_cycles; + + // The mode determining how bad blocks fail + uint8_t badblock_behavior; + + // Number of write operations (erase/prog) before forcefully killing + // the program with exit. Simulates power-loss. 0 disables. + uint32_t power_cycles; + + // Optional buffer for RAM block device. + void *buffer; + + // Optional buffer for wear + void *wear_buffer; +}; + +// testbd state +typedef struct lfs2_testbd { + union { + struct { + lfs2_filebd_t bd; + struct lfs2_filebd_config cfg; + } file; + struct { + lfs2_rambd_t bd; + struct lfs2_rambd_config cfg; + } ram; + } u; + + bool persist; + uint32_t power_cycles; + lfs2_testbd_wear_t *wear; + + const struct lfs2_testbd_config *cfg; +} lfs2_testbd_t; + + +/// Block device API /// + +// Create a test block device using the geometry in lfs2_config +// +// Note that filebd is used if a path is provided, if path is NULL +// testbd will use rambd which can be much faster. +int lfs2_testbd_create(const struct lfs2_config *cfg, const char *path); +int lfs2_testbd_createcfg(const struct lfs2_config *cfg, const char *path, + const struct lfs2_testbd_config *bdcfg); + +// Clean up memory associated with block device +int lfs2_testbd_destroy(const struct lfs2_config *cfg); + +// Read a block +int lfs2_testbd_read(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size); + +// Program a block +// +// The block must have previously been erased. +int lfs2_testbd_prog(const struct lfs2_config *cfg, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_size_t size); + +// Erase a block +// +// A block must be erased before being programmed. The +// state of an erased block is undefined. +int lfs2_testbd_erase(const struct lfs2_config *cfg, lfs2_block_t block); + +// Sync the block device +int lfs2_testbd_sync(const struct lfs2_config *cfg); + + +/// Additional extended API for driving test features /// + +// Get simulated wear on a given block +lfs2_testbd_swear_t lfs2_testbd_getwear(const struct lfs2_config *cfg, + lfs2_block_t block); + +// Manually set simulated wear on a given block +int lfs2_testbd_setwear(const struct lfs2_config *cfg, + lfs2_block_t block, lfs2_testbd_wear_t wear); + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/features/storage/filesystem/littlefsv2/littlefs/lfs2.c b/features/storage/filesystem/littlefsv2/littlefs/lfs2.c new file mode 100644 index 00000000000..6bae806ceb4 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/lfs2.c @@ -0,0 +1,4913 @@ +/* + * The little filesystem + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "lfs2.h" +#include "lfs2_util.h" + +#define LFS2_BLOCK_NULL ((lfs2_block_t)-1) +#define LFS2_BLOCK_INLINE ((lfs2_block_t)-2) + +/// Caching block device operations /// +static inline void lfs2_cache_drop(lfs2_t *lfs2, lfs2_cache_t *rcache) { + // do not zero, cheaper if cache is readonly or only going to be + // written with identical data (during relocates) + (void)lfs2; + rcache->block = LFS2_BLOCK_NULL; +} + +static inline void lfs2_cache_zero(lfs2_t *lfs2, lfs2_cache_t *pcache) { + // zero to avoid information leak + memset(pcache->buffer, 0xff, lfs2->cfg->cache_size); + pcache->block = LFS2_BLOCK_NULL; +} + +static int lfs2_bd_read(lfs2_t *lfs2, + const lfs2_cache_t *pcache, lfs2_cache_t *rcache, lfs2_size_t hint, + lfs2_block_t block, lfs2_off_t off, + void *buffer, lfs2_size_t size) { + uint8_t *data = buffer; + if (block >= lfs2->cfg->block_count || + off+size > lfs2->cfg->block_size) { + return LFS2_ERR_CORRUPT; + } + + while (size > 0) { + lfs2_size_t diff = size; + + if (pcache && block == pcache->block && + off < pcache->off + pcache->size) { + if (off >= pcache->off) { + // is already in pcache? + diff = lfs2_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 = lfs2_min(diff, pcache->off-off); + } + + if (block == rcache->block && + off < rcache->off + rcache->size) { + if (off >= rcache->off) { + // is already in rcache? + diff = lfs2_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 = lfs2_min(diff, rcache->off-off); + } + + if (size >= hint && off % lfs2->cfg->read_size == 0 && + size >= lfs2->cfg->read_size) { + // bypass cache? + diff = lfs2_aligndown(diff, lfs2->cfg->read_size); + int err = lfs2->cfg->read(lfs2->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 + LFS2_ASSERT(block < lfs2->cfg->block_count); + rcache->block = block; + rcache->off = lfs2_aligndown(off, lfs2->cfg->read_size); + rcache->size = lfs2_min( + lfs2_min( + lfs2_alignup(off+hint, lfs2->cfg->read_size), + lfs2->cfg->block_size) + - rcache->off, + lfs2->cfg->cache_size); + int err = lfs2->cfg->read(lfs2->cfg, rcache->block, + rcache->off, rcache->buffer, rcache->size); + LFS2_ASSERT(err <= 0); + if (err) { + return err; + } + } + + return 0; +} + +enum { + LFS2_CMP_EQ = 0, + LFS2_CMP_LT = 1, + LFS2_CMP_GT = 2, +}; + +static int lfs2_bd_cmp(lfs2_t *lfs2, + const lfs2_cache_t *pcache, lfs2_cache_t *rcache, lfs2_size_t hint, + lfs2_block_t block, lfs2_off_t off, + const void *buffer, lfs2_size_t size) { + const uint8_t *data = buffer; + + for (lfs2_off_t i = 0; i < size; i++) { + uint8_t dat; + int err = lfs2_bd_read(lfs2, + pcache, rcache, hint-i, + block, off+i, &dat, 1); + if (err) { + return err; + } + + if (dat != data[i]) { + return (dat < data[i]) ? LFS2_CMP_LT : LFS2_CMP_GT; + } + } + + return LFS2_CMP_EQ; +} + +static int lfs2_bd_flush(lfs2_t *lfs2, + lfs2_cache_t *pcache, lfs2_cache_t *rcache, bool validate) { + if (pcache->block != LFS2_BLOCK_NULL && pcache->block != LFS2_BLOCK_INLINE) { + LFS2_ASSERT(pcache->block < lfs2->cfg->block_count); + lfs2_size_t diff = lfs2_alignup(pcache->size, lfs2->cfg->prog_size); + int err = lfs2->cfg->prog(lfs2->cfg, pcache->block, + pcache->off, pcache->buffer, diff); + LFS2_ASSERT(err <= 0); + if (err) { + return err; + } + + if (validate) { + // check data on disk + lfs2_cache_drop(lfs2, rcache); + int res = lfs2_bd_cmp(lfs2, + NULL, rcache, diff, + pcache->block, pcache->off, pcache->buffer, diff); + if (res < 0) { + return res; + } + + if (res != LFS2_CMP_EQ) { + return LFS2_ERR_CORRUPT; + } + } + + lfs2_cache_zero(lfs2, pcache); + } + + return 0; +} + +static int lfs2_bd_sync(lfs2_t *lfs2, + lfs2_cache_t *pcache, lfs2_cache_t *rcache, bool validate) { + lfs2_cache_drop(lfs2, rcache); + + int err = lfs2_bd_flush(lfs2, pcache, rcache, validate); + if (err) { + return err; + } + + err = lfs2->cfg->sync(lfs2->cfg); + LFS2_ASSERT(err <= 0); + return err; +} + +static int lfs2_bd_prog(lfs2_t *lfs2, + lfs2_cache_t *pcache, lfs2_cache_t *rcache, bool validate, + lfs2_block_t block, lfs2_off_t off, + const void *buffer, lfs2_size_t size) { + const uint8_t *data = buffer; + LFS2_ASSERT(block == LFS2_BLOCK_INLINE || block < lfs2->cfg->block_count); + LFS2_ASSERT(off + size <= lfs2->cfg->block_size); + + while (size > 0) { + if (block == pcache->block && + off >= pcache->off && + off < pcache->off + lfs2->cfg->cache_size) { + // already fits in pcache? + lfs2_size_t diff = lfs2_min(size, + lfs2->cfg->cache_size - (off-pcache->off)); + memcpy(&pcache->buffer[off-pcache->off], data, diff); + + data += diff; + off += diff; + size -= diff; + + pcache->size = lfs2_max(pcache->size, off - pcache->off); + if (pcache->size == lfs2->cfg->cache_size) { + // eagerly flush out pcache if we fill up + int err = lfs2_bd_flush(lfs2, pcache, rcache, validate); + if (err) { + return err; + } + } + + continue; + } + + // pcache must have been flushed, either by programming and + // entire block or manually flushing the pcache + LFS2_ASSERT(pcache->block == LFS2_BLOCK_NULL); + + // prepare pcache, first condition can no longer fail + pcache->block = block; + pcache->off = lfs2_aligndown(off, lfs2->cfg->prog_size); + pcache->size = 0; + } + + return 0; +} + +static int lfs2_bd_erase(lfs2_t *lfs2, lfs2_block_t block) { + LFS2_ASSERT(block < lfs2->cfg->block_count); + int err = lfs2->cfg->erase(lfs2->cfg, block); + LFS2_ASSERT(err <= 0); + return err; +} + + +/// Small type-level utilities /// +// operations on block pairs +static inline void lfs2_pair_swap(lfs2_block_t pair[2]) { + lfs2_block_t t = pair[0]; + pair[0] = pair[1]; + pair[1] = t; +} + +static inline bool lfs2_pair_isnull(const lfs2_block_t pair[2]) { + return pair[0] == LFS2_BLOCK_NULL || pair[1] == LFS2_BLOCK_NULL; +} + +static inline int lfs2_pair_cmp( + const lfs2_block_t paira[2], + const lfs2_block_t pairb[2]) { + return !(paira[0] == pairb[0] || paira[1] == pairb[1] || + paira[0] == pairb[1] || paira[1] == pairb[0]); +} + +static inline bool lfs2_pair_sync( + const lfs2_block_t paira[2], + const lfs2_block_t pairb[2]) { + return (paira[0] == pairb[0] && paira[1] == pairb[1]) || + (paira[0] == pairb[1] && paira[1] == pairb[0]); +} + +static inline void lfs2_pair_fromle32(lfs2_block_t pair[2]) { + pair[0] = lfs2_fromle32(pair[0]); + pair[1] = lfs2_fromle32(pair[1]); +} + +static inline void lfs2_pair_tole32(lfs2_block_t pair[2]) { + pair[0] = lfs2_tole32(pair[0]); + pair[1] = lfs2_tole32(pair[1]); +} + +// operations on 32-bit entry tags +typedef uint32_t lfs2_tag_t; +typedef int32_t lfs2_stag_t; + +#define LFS2_MKTAG(type, id, size) \ + (((lfs2_tag_t)(type) << 20) | ((lfs2_tag_t)(id) << 10) | (lfs2_tag_t)(size)) + +#define LFS2_MKTAG_IF(cond, type, id, size) \ + ((cond) ? LFS2_MKTAG(type, id, size) : LFS2_MKTAG(LFS2_FROM_NOOP, 0, 0)) + +#define LFS2_MKTAG_IF_ELSE(cond, type1, id1, size1, type2, id2, size2) \ + ((cond) ? LFS2_MKTAG(type1, id1, size1) : LFS2_MKTAG(type2, id2, size2)) + +static inline bool lfs2_tag_isvalid(lfs2_tag_t tag) { + return !(tag & 0x80000000); +} + +static inline bool lfs2_tag_isdelete(lfs2_tag_t tag) { + return ((int32_t)(tag << 22) >> 22) == -1; +} + +static inline uint16_t lfs2_tag_type1(lfs2_tag_t tag) { + return (tag & 0x70000000) >> 20; +} + +static inline uint16_t lfs2_tag_type3(lfs2_tag_t tag) { + return (tag & 0x7ff00000) >> 20; +} + +static inline uint8_t lfs2_tag_chunk(lfs2_tag_t tag) { + return (tag & 0x0ff00000) >> 20; +} + +static inline int8_t lfs2_tag_splice(lfs2_tag_t tag) { + return (int8_t)lfs2_tag_chunk(tag); +} + +static inline uint16_t lfs2_tag_id(lfs2_tag_t tag) { + return (tag & 0x000ffc00) >> 10; +} + +static inline lfs2_size_t lfs2_tag_size(lfs2_tag_t tag) { + return tag & 0x000003ff; +} + +static inline lfs2_size_t lfs2_tag_dsize(lfs2_tag_t tag) { + return sizeof(tag) + lfs2_tag_size(tag + lfs2_tag_isdelete(tag)); +} + +// operations on attributes in attribute lists +struct lfs2_mattr { + lfs2_tag_t tag; + const void *buffer; +}; + +struct lfs2_diskoff { + lfs2_block_t block; + lfs2_off_t off; +}; + +#define LFS2_MKATTRS(...) \ + (struct lfs2_mattr[]){__VA_ARGS__}, \ + sizeof((struct lfs2_mattr[]){__VA_ARGS__}) / sizeof(struct lfs2_mattr) + +// operations on global state +static inline void lfs2_gstate_xor(lfs2_gstate_t *a, const lfs2_gstate_t *b) { + for (int i = 0; i < 3; i++) { + ((uint32_t*)a)[i] ^= ((const uint32_t*)b)[i]; + } +} + +static inline bool lfs2_gstate_iszero(const lfs2_gstate_t *a) { + for (int i = 0; i < 3; i++) { + if (((uint32_t*)a)[i] != 0) { + return false; + } + } + return true; +} + +static inline bool lfs2_gstate_hasorphans(const lfs2_gstate_t *a) { + return lfs2_tag_size(a->tag); +} + +static inline uint8_t lfs2_gstate_getorphans(const lfs2_gstate_t *a) { + return lfs2_tag_size(a->tag); +} + +static inline bool lfs2_gstate_hasmove(const lfs2_gstate_t *a) { + return lfs2_tag_type1(a->tag); +} + +static inline bool lfs2_gstate_hasmovehere(const lfs2_gstate_t *a, + const lfs2_block_t *pair) { + return lfs2_tag_type1(a->tag) && lfs2_pair_cmp(a->pair, pair) == 0; +} + +static inline void lfs2_gstate_fromle32(lfs2_gstate_t *a) { + a->tag = lfs2_fromle32(a->tag); + a->pair[0] = lfs2_fromle32(a->pair[0]); + a->pair[1] = lfs2_fromle32(a->pair[1]); +} + +static inline void lfs2_gstate_tole32(lfs2_gstate_t *a) { + a->tag = lfs2_tole32(a->tag); + a->pair[0] = lfs2_tole32(a->pair[0]); + a->pair[1] = lfs2_tole32(a->pair[1]); +} + +// other endianness operations +static void lfs2_ctz_fromle32(struct lfs2_ctz *ctz) { + ctz->head = lfs2_fromle32(ctz->head); + ctz->size = lfs2_fromle32(ctz->size); +} + +static void lfs2_ctz_tole32(struct lfs2_ctz *ctz) { + ctz->head = lfs2_tole32(ctz->head); + ctz->size = lfs2_tole32(ctz->size); +} + +static inline void lfs2_superblock_fromle32(lfs2_superblock_t *superblock) { + superblock->version = lfs2_fromle32(superblock->version); + superblock->block_size = lfs2_fromle32(superblock->block_size); + superblock->block_count = lfs2_fromle32(superblock->block_count); + superblock->name_max = lfs2_fromle32(superblock->name_max); + superblock->file_max = lfs2_fromle32(superblock->file_max); + superblock->attr_max = lfs2_fromle32(superblock->attr_max); +} + +static inline void lfs2_superblock_tole32(lfs2_superblock_t *superblock) { + superblock->version = lfs2_tole32(superblock->version); + superblock->block_size = lfs2_tole32(superblock->block_size); + superblock->block_count = lfs2_tole32(superblock->block_count); + superblock->name_max = lfs2_tole32(superblock->name_max); + superblock->file_max = lfs2_tole32(superblock->file_max); + superblock->attr_max = lfs2_tole32(superblock->attr_max); +} + + +/// Internal operations predeclared here /// +static int lfs2_dir_commit(lfs2_t *lfs2, lfs2_mdir_t *dir, + const struct lfs2_mattr *attrs, int attrcount); +static int lfs2_dir_compact(lfs2_t *lfs2, + lfs2_mdir_t *dir, const struct lfs2_mattr *attrs, int attrcount, + lfs2_mdir_t *source, uint16_t begin, uint16_t end); +static int lfs2_file_outline(lfs2_t *lfs2, lfs2_file_t *file); +static int lfs2_file_flush(lfs2_t *lfs2, lfs2_file_t *file); +static void lfs2_fs_preporphans(lfs2_t *lfs2, int8_t orphans); +static void lfs2_fs_prepmove(lfs2_t *lfs2, + uint16_t id, const lfs2_block_t pair[2]); +static int lfs2_fs_pred(lfs2_t *lfs2, const lfs2_block_t dir[2], + lfs2_mdir_t *pdir); +static lfs2_stag_t lfs2_fs_parent(lfs2_t *lfs2, const lfs2_block_t dir[2], + lfs2_mdir_t *parent); +static int lfs2_fs_relocate(lfs2_t *lfs2, + const lfs2_block_t oldpair[2], lfs2_block_t newpair[2]); +int lfs2_fs_traverseraw(lfs2_t *lfs2, + int (*cb)(void *data, lfs2_block_t block), void *data, + bool includeorphans); +static int lfs2_fs_forceconsistency(lfs2_t *lfs2); +static int lfs2_deinit(lfs2_t *lfs2); +#ifdef LFS2_MIGRATE +static int lfs21_traverse(lfs2_t *lfs2, + int (*cb)(void*, lfs2_block_t), void *data); +#endif + +/// Block allocator /// +static int lfs2_alloc_lookahead(void *p, lfs2_block_t block) { + lfs2_t *lfs2 = (lfs2_t*)p; + lfs2_block_t off = ((block - lfs2->free.off) + + lfs2->cfg->block_count) % lfs2->cfg->block_count; + + if (off < lfs2->free.size) { + lfs2->free.buffer[off / 32] |= 1U << (off % 32); + } + + return 0; +} + +static void lfs2_alloc_ack(lfs2_t *lfs2) { + lfs2->free.ack = lfs2->cfg->block_count; +} + +// Invalidate the lookahead buffer. This is done during mounting and +// failed traversals +static void lfs2_alloc_reset(lfs2_t *lfs2) { + lfs2->free.off = lfs2->seed % lfs2->cfg->block_size; + lfs2->free.size = 0; + lfs2->free.i = 0; + lfs2_alloc_ack(lfs2); +} + +static int lfs2_alloc(lfs2_t *lfs2, lfs2_block_t *block) { + while (true) { + while (lfs2->free.i != lfs2->free.size) { + lfs2_block_t off = lfs2->free.i; + lfs2->free.i += 1; + lfs2->free.ack -= 1; + + if (!(lfs2->free.buffer[off / 32] & (1U << (off % 32)))) { + // found a free block + *block = (lfs2->free.off + off) % lfs2->cfg->block_count; + + // eagerly find next off so an alloc ack can + // discredit old lookahead blocks + while (lfs2->free.i != lfs2->free.size && + (lfs2->free.buffer[lfs2->free.i / 32] + & (1U << (lfs2->free.i % 32)))) { + lfs2->free.i += 1; + lfs2->free.ack -= 1; + } + + return 0; + } + } + + // check if we have looked at all blocks since last ack + if (lfs2->free.ack == 0) { + LFS2_ERROR("No more free space %"PRIu32, + lfs2->free.i + lfs2->free.off); + return LFS2_ERR_NOSPC; + } + + lfs2->free.off = (lfs2->free.off + lfs2->free.size) + % lfs2->cfg->block_count; + lfs2->free.size = lfs2_min(8*lfs2->cfg->lookahead_size, lfs2->free.ack); + lfs2->free.i = 0; + + // find mask of free blocks from tree + memset(lfs2->free.buffer, 0, lfs2->cfg->lookahead_size); + int err = lfs2_fs_traverseraw(lfs2, lfs2_alloc_lookahead, lfs2, true); + if (err) { + lfs2_alloc_reset(lfs2); + return err; + } + } +} + +/// Metadata pair and directory operations /// +static lfs2_stag_t lfs2_dir_getslice(lfs2_t *lfs2, const lfs2_mdir_t *dir, + lfs2_tag_t gmask, lfs2_tag_t gtag, + lfs2_off_t goff, void *gbuffer, lfs2_size_t gsize) { + lfs2_off_t off = dir->off; + lfs2_tag_t ntag = dir->etag; + lfs2_stag_t gdiff = 0; + + if (lfs2_gstate_hasmovehere(&lfs2->gdisk, dir->pair) && + lfs2_tag_id(gmask) != 0 && + lfs2_tag_id(lfs2->gdisk.tag) <= lfs2_tag_id(gtag)) { + // synthetic moves + gdiff -= LFS2_MKTAG(0, 1, 0); + } + + // iterate over dir block backwards (for faster lookups) + while (off >= sizeof(lfs2_tag_t) + lfs2_tag_dsize(ntag)) { + off -= lfs2_tag_dsize(ntag); + lfs2_tag_t tag = ntag; + int err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, sizeof(ntag), + dir->pair[0], off, &ntag, sizeof(ntag)); + if (err) { + return err; + } + + ntag = (lfs2_frombe32(ntag) ^ tag) & 0x7fffffff; + + if (lfs2_tag_id(gmask) != 0 && + lfs2_tag_type1(tag) == LFS2_TYPE_SPLICE && + lfs2_tag_id(tag) <= lfs2_tag_id(gtag - gdiff)) { + if (tag == (LFS2_MKTAG(LFS2_TYPE_CREATE, 0, 0) | + (LFS2_MKTAG(0, 0x3ff, 0) & (gtag - gdiff)))) { + // found where we were created + return LFS2_ERR_NOENT; + } + + // move around splices + gdiff += LFS2_MKTAG(0, lfs2_tag_splice(tag), 0); + } + + if ((gmask & tag) == (gmask & (gtag - gdiff))) { + if (lfs2_tag_isdelete(tag)) { + return LFS2_ERR_NOENT; + } + + lfs2_size_t diff = lfs2_min(lfs2_tag_size(tag), gsize); + err = lfs2_bd_read(lfs2, + NULL, &lfs2->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 LFS2_ERR_NOENT; +} + +static lfs2_stag_t lfs2_dir_get(lfs2_t *lfs2, const lfs2_mdir_t *dir, + lfs2_tag_t gmask, lfs2_tag_t gtag, void *buffer) { + return lfs2_dir_getslice(lfs2, dir, + gmask, gtag, + 0, buffer, lfs2_tag_size(gtag)); +} + +static int lfs2_dir_getread(lfs2_t *lfs2, const lfs2_mdir_t *dir, + const lfs2_cache_t *pcache, lfs2_cache_t *rcache, lfs2_size_t hint, + lfs2_tag_t gmask, lfs2_tag_t gtag, + lfs2_off_t off, void *buffer, lfs2_size_t size) { + uint8_t *data = buffer; + if (off+size > lfs2->cfg->block_size) { + return LFS2_ERR_CORRUPT; + } + + while (size > 0) { + lfs2_size_t diff = size; + + if (pcache && pcache->block == LFS2_BLOCK_INLINE && + off < pcache->off + pcache->size) { + if (off >= pcache->off) { + // is already in pcache? + diff = lfs2_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 = lfs2_min(diff, pcache->off-off); + } + + if (rcache->block == LFS2_BLOCK_INLINE && + off < rcache->off + rcache->size) { + if (off >= rcache->off) { + // is already in rcache? + diff = lfs2_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 = lfs2_min(diff, rcache->off-off); + } + + // load to cache, first condition can no longer fail + rcache->block = LFS2_BLOCK_INLINE; + rcache->off = lfs2_aligndown(off, lfs2->cfg->read_size); + rcache->size = lfs2_min(lfs2_alignup(off+hint, lfs2->cfg->read_size), + lfs2->cfg->cache_size); + int err = lfs2_dir_getslice(lfs2, dir, gmask, gtag, + rcache->off, rcache->buffer, rcache->size); + if (err < 0) { + return err; + } + } + + return 0; +} + +static int lfs2_dir_traverse_filter(void *p, + lfs2_tag_t tag, const void *buffer) { + lfs2_tag_t *filtertag = p; + (void)buffer; + + // which mask depends on unique bit in tag structure + uint32_t mask = (tag & LFS2_MKTAG(0x100, 0, 0)) + ? LFS2_MKTAG(0x7ff, 0x3ff, 0) + : LFS2_MKTAG(0x700, 0x3ff, 0); + + // check for redundancy + if ((mask & tag) == (mask & *filtertag) || + lfs2_tag_isdelete(*filtertag) || + (LFS2_MKTAG(0x7ff, 0x3ff, 0) & tag) == ( + LFS2_MKTAG(LFS2_TYPE_DELETE, 0, 0) | + (LFS2_MKTAG(0, 0x3ff, 0) & *filtertag))) { + return true; + } + + // check if we need to adjust for created/deleted tags + if (lfs2_tag_type1(tag) == LFS2_TYPE_SPLICE && + lfs2_tag_id(tag) <= lfs2_tag_id(*filtertag)) { + *filtertag += LFS2_MKTAG(0, lfs2_tag_splice(tag), 0); + } + + return false; +} + +static int lfs2_dir_traverse(lfs2_t *lfs2, + const lfs2_mdir_t *dir, lfs2_off_t off, lfs2_tag_t ptag, + const struct lfs2_mattr *attrs, int attrcount, + lfs2_tag_t tmask, lfs2_tag_t ttag, + uint16_t begin, uint16_t end, int16_t diff, + int (*cb)(void *data, lfs2_tag_t tag, const void *buffer), void *data) { + // iterate over directory and attrs + while (true) { + lfs2_tag_t tag; + const void *buffer; + struct lfs2_diskoff disk; + if (off+lfs2_tag_dsize(ptag) < dir->off) { + off += lfs2_tag_dsize(ptag); + int err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, sizeof(tag), + dir->pair[0], off, &tag, sizeof(tag)); + if (err) { + return err; + } + + tag = (lfs2_frombe32(tag) ^ ptag) | 0x80000000; + disk.block = dir->pair[0]; + disk.off = off+sizeof(lfs2_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; + } + + lfs2_tag_t mask = LFS2_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 (lfs2_tag_id(tmask) != 0) { + // scan for duplicates and update tag based on creates/deletes + int filter = lfs2_dir_traverse(lfs2, + dir, off, ptag, attrs, attrcount, + 0, 0, 0, 0, 0, + lfs2_dir_traverse_filter, &tag); + if (filter < 0) { + return filter; + } + + if (filter) { + continue; + } + + // in filter range? + if (!(lfs2_tag_id(tag) >= begin && lfs2_tag_id(tag) < end)) { + continue; + } + } + + // handle special cases for mcu-side operations + if (lfs2_tag_type3(tag) == LFS2_FROM_NOOP) { + // do nothing + } else if (lfs2_tag_type3(tag) == LFS2_FROM_MOVE) { + uint16_t fromid = lfs2_tag_size(tag); + uint16_t toid = lfs2_tag_id(tag); + int err = lfs2_dir_traverse(lfs2, + buffer, 0, 0xffffffff, NULL, 0, + LFS2_MKTAG(0x600, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, 0, 0), + fromid, fromid+1, toid-fromid+diff, + cb, data); + if (err) { + return err; + } + } else if (lfs2_tag_type3(tag) == LFS2_FROM_USERATTRS) { + for (unsigned i = 0; i < lfs2_tag_size(tag); i++) { + const struct lfs2_attr *a = buffer; + int err = cb(data, LFS2_MKTAG(LFS2_TYPE_USERATTR + a[i].type, + lfs2_tag_id(tag) + diff, a[i].size), a[i].buffer); + if (err) { + return err; + } + } + } else { + int err = cb(data, tag + LFS2_MKTAG(0, diff, 0), buffer); + if (err) { + return err; + } + } + } +} + +static lfs2_stag_t lfs2_dir_fetchmatch(lfs2_t *lfs2, + lfs2_mdir_t *dir, const lfs2_block_t pair[2], + lfs2_tag_t fmask, lfs2_tag_t ftag, uint16_t *id, + int (*cb)(void *data, lfs2_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 + lfs2_stag_t besttag = -1; + + // if either block address is invalid we return LFS2_ERR_CORRUPT here, + // otherwise later writes to the pair could fail + if (pair[0] >= lfs2->cfg->block_count || pair[1] >= lfs2->cfg->block_count) { + return LFS2_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 = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, sizeof(revs[i]), + pair[i], 0, &revs[i], sizeof(revs[i])); + revs[i] = lfs2_fromle32(revs[i]); + if (err && err != LFS2_ERR_CORRUPT) { + return err; + } + + if (err != LFS2_ERR_CORRUPT && + lfs2_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++) { + lfs2_off_t off = 0; + lfs2_tag_t ptag = 0xffffffff; + + uint16_t tempcount = 0; + lfs2_block_t temptail[2] = {LFS2_BLOCK_NULL, LFS2_BLOCK_NULL}; + bool tempsplit = false; + lfs2_stag_t tempbesttag = besttag; + + dir->rev = lfs2_tole32(dir->rev); + uint32_t crc = lfs2_crc(0xffffffff, &dir->rev, sizeof(dir->rev)); + dir->rev = lfs2_fromle32(dir->rev); + + while (true) { + // extract next tag + lfs2_tag_t tag; + off += lfs2_tag_dsize(ptag); + int err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, lfs2->cfg->block_size, + dir->pair[0], off, &tag, sizeof(tag)); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + // can't continue? + dir->erased = false; + break; + } + return err; + } + + crc = lfs2_crc(crc, &tag, sizeof(tag)); + tag = lfs2_frombe32(tag) ^ ptag; + + // next commit not yet programmed or we're not in valid range + if (!lfs2_tag_isvalid(tag)) { + dir->erased = (lfs2_tag_type1(ptag) == LFS2_TYPE_CRC && + dir->off % lfs2->cfg->prog_size == 0); + break; + } else if (off + lfs2_tag_dsize(tag) > lfs2->cfg->block_size) { + dir->erased = false; + break; + } + + ptag = tag; + + if (lfs2_tag_type1(tag) == LFS2_TYPE_CRC) { + // check the crc attr + uint32_t dcrc; + err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, lfs2->cfg->block_size, + dir->pair[0], off+sizeof(tag), &dcrc, sizeof(dcrc)); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + dir->erased = false; + break; + } + return err; + } + dcrc = lfs2_fromle32(dcrc); + + if (crc != dcrc) { + dir->erased = false; + break; + } + + // reset the next bit if we need to + ptag ^= (lfs2_tag_t)(lfs2_tag_chunk(tag) & 1U) << 31; + + // toss our crc into the filesystem seed for + // pseudorandom numbers + lfs2->seed ^= crc; + + // update with what's found so far + besttag = tempbesttag; + dir->off = off + lfs2_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 (lfs2_off_t j = sizeof(tag); j < lfs2_tag_dsize(tag); j++) { + uint8_t dat; + err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, lfs2->cfg->block_size, + dir->pair[0], off+j, &dat, 1); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + dir->erased = false; + break; + } + return err; + } + + crc = lfs2_crc(crc, &dat, 1); + } + + // directory modification tags? + if (lfs2_tag_type1(tag) == LFS2_TYPE_NAME) { + // increase count of files if necessary + if (lfs2_tag_id(tag) >= tempcount) { + tempcount = lfs2_tag_id(tag) + 1; + } + } else if (lfs2_tag_type1(tag) == LFS2_TYPE_SPLICE) { + tempcount += lfs2_tag_splice(tag); + + if (tag == (LFS2_MKTAG(LFS2_TYPE_DELETE, 0, 0) | + (LFS2_MKTAG(0, 0x3ff, 0) & tempbesttag))) { + tempbesttag |= 0x80000000; + } else if (tempbesttag != -1 && + lfs2_tag_id(tag) <= lfs2_tag_id(tempbesttag)) { + tempbesttag += LFS2_MKTAG(0, lfs2_tag_splice(tag), 0); + } + } else if (lfs2_tag_type1(tag) == LFS2_TYPE_TAIL) { + tempsplit = (lfs2_tag_chunk(tag) & 1); + + err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, lfs2->cfg->block_size, + dir->pair[0], off+sizeof(tag), &temptail, 8); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + dir->erased = false; + break; + } + } + lfs2_pair_fromle32(temptail); + } + + // found a match for our fetcher? + if ((fmask & tag) == (fmask & ftag)) { + int res = cb(data, tag, &(struct lfs2_diskoff){ + dir->pair[0], off+sizeof(tag)}); + if (res < 0) { + if (res == LFS2_ERR_CORRUPT) { + dir->erased = false; + break; + } + return res; + } + + if (res == LFS2_CMP_EQ) { + // found a match + tempbesttag = tag; + } else if ((LFS2_MKTAG(0x7ff, 0x3ff, 0) & tag) == + (LFS2_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 == LFS2_CMP_GT && + lfs2_tag_id(tag) <= lfs2_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 (lfs2_gstate_hasmovehere(&lfs2->gdisk, dir->pair)) { + if (lfs2_tag_id(lfs2->gdisk.tag) == lfs2_tag_id(besttag)) { + besttag |= 0x80000000; + } else if (besttag != -1 && + lfs2_tag_id(lfs2->gdisk.tag) < lfs2_tag_id(besttag)) { + besttag -= LFS2_MKTAG(0, 1, 0); + } + } + + // found tag? or found best id? + if (id) { + *id = lfs2_min(lfs2_tag_id(besttag), dir->count); + } + + if (lfs2_tag_isvalid(besttag)) { + return besttag; + } else if (lfs2_tag_id(besttag) < dir->count) { + return LFS2_ERR_NOENT; + } else { + return 0; + } + } + + // failed, try the other block? + lfs2_pair_swap(dir->pair); + dir->rev = revs[(r+1)%2]; + } + + LFS2_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}", + dir->pair[0], dir->pair[1]); + return LFS2_ERR_CORRUPT; +} + +static int lfs2_dir_fetch(lfs2_t *lfs2, + lfs2_mdir_t *dir, const lfs2_block_t pair[2]) { + // note, mask=-1, tag=-1 can never match a tag since this + // pattern has the invalid bit set + return (int)lfs2_dir_fetchmatch(lfs2, dir, pair, + (lfs2_tag_t)-1, (lfs2_tag_t)-1, NULL, NULL, NULL); +} + +static int lfs2_dir_getgstate(lfs2_t *lfs2, const lfs2_mdir_t *dir, + lfs2_gstate_t *gstate) { + lfs2_gstate_t temp; + lfs2_stag_t res = lfs2_dir_get(lfs2, dir, LFS2_MKTAG(0x7ff, 0, 0), + LFS2_MKTAG(LFS2_TYPE_MOVESTATE, 0, sizeof(temp)), &temp); + if (res < 0 && res != LFS2_ERR_NOENT) { + return res; + } + + if (res != LFS2_ERR_NOENT) { + // xor together to find resulting gstate + lfs2_gstate_fromle32(&temp); + lfs2_gstate_xor(gstate, &temp); + } + + return 0; +} + +static int lfs2_dir_getinfo(lfs2_t *lfs2, lfs2_mdir_t *dir, + uint16_t id, struct lfs2_info *info) { + if (id == 0x3ff) { + // special case for root + strcpy(info->name, "/"); + info->type = LFS2_TYPE_DIR; + return 0; + } + + lfs2_stag_t tag = lfs2_dir_get(lfs2, dir, LFS2_MKTAG(0x780, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_NAME, id, lfs2->name_max+1), info->name); + if (tag < 0) { + return (int)tag; + } + + info->type = lfs2_tag_type3(tag); + + struct lfs2_ctz ctz; + tag = lfs2_dir_get(lfs2, dir, LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, id, sizeof(ctz)), &ctz); + if (tag < 0) { + return (int)tag; + } + lfs2_ctz_fromle32(&ctz); + + if (lfs2_tag_type3(tag) == LFS2_TYPE_CTZSTRUCT) { + info->size = ctz.size; + } else if (lfs2_tag_type3(tag) == LFS2_TYPE_INLINESTRUCT) { + info->size = lfs2_tag_size(tag); + } + + return 0; +} + +struct lfs2_dir_find_match { + lfs2_t *lfs2; + const void *name; + lfs2_size_t size; +}; + +static int lfs2_dir_find_match(void *data, + lfs2_tag_t tag, const void *buffer) { + struct lfs2_dir_find_match *name = data; + lfs2_t *lfs2 = name->lfs2; + const struct lfs2_diskoff *disk = buffer; + + // compare with disk + lfs2_size_t diff = lfs2_min(name->size, lfs2_tag_size(tag)); + int res = lfs2_bd_cmp(lfs2, + NULL, &lfs2->rcache, diff, + disk->block, disk->off, name->name, diff); + if (res != LFS2_CMP_EQ) { + return res; + } + + // only equal if our size is still the same + if (name->size != lfs2_tag_size(tag)) { + return (name->size < lfs2_tag_size(tag)) ? LFS2_CMP_LT : LFS2_CMP_GT; + } + + // found a match! + return LFS2_CMP_EQ; +} + +static lfs2_stag_t lfs2_dir_find(lfs2_t *lfs2, lfs2_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 + lfs2_stag_t tag = LFS2_MKTAG(LFS2_TYPE_DIR, 0x3ff, 0); + dir->tail[0] = lfs2->root[0]; + dir->tail[1] = lfs2->root[1]; + + while (true) { +nextname: + // skip slashes + name += strspn(name, "/"); + lfs2_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; + lfs2_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 (lfs2_tag_type3(tag) != LFS2_TYPE_DIR) { + return LFS2_ERR_NOTDIR; + } + + // grab the entry data + if (lfs2_tag_id(tag) != 0x3ff) { + lfs2_stag_t res = lfs2_dir_get(lfs2, dir, LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, lfs2_tag_id(tag), 8), dir->tail); + if (res < 0) { + return res; + } + lfs2_pair_fromle32(dir->tail); + } + + // find entry matching name + while (true) { + tag = lfs2_dir_fetchmatch(lfs2, dir, dir->tail, + LFS2_MKTAG(0x780, 0, 0), + LFS2_MKTAG(LFS2_TYPE_NAME, 0, namelen), + // are we last name? + (strchr(name, '/') == NULL) ? id : NULL, + lfs2_dir_find_match, &(struct lfs2_dir_find_match){ + lfs2, name, namelen}); + if (tag < 0) { + return tag; + } + + if (tag) { + break; + } + + if (!dir->split) { + return LFS2_ERR_NOENT; + } + } + + // to next name + name += namelen; + } +} + +// commit logic +struct lfs2_commit { + lfs2_block_t block; + lfs2_off_t off; + lfs2_tag_t ptag; + uint32_t crc; + + lfs2_off_t begin; + lfs2_off_t end; +}; + +static int lfs2_dir_commitprog(lfs2_t *lfs2, struct lfs2_commit *commit, + const void *buffer, lfs2_size_t size) { + int err = lfs2_bd_prog(lfs2, + &lfs2->pcache, &lfs2->rcache, false, + commit->block, commit->off , + (const uint8_t*)buffer, size); + if (err) { + return err; + } + + commit->crc = lfs2_crc(commit->crc, buffer, size); + commit->off += size; + return 0; +} + +static int lfs2_dir_commitattr(lfs2_t *lfs2, struct lfs2_commit *commit, + lfs2_tag_t tag, const void *buffer) { + // check if we fit + lfs2_size_t dsize = lfs2_tag_dsize(tag); + if (commit->off + dsize > commit->end) { + return LFS2_ERR_NOSPC; + } + + // write out tag + lfs2_tag_t ntag = lfs2_tobe32((tag & 0x7fffffff) ^ commit->ptag); + int err = lfs2_dir_commitprog(lfs2, commit, &ntag, sizeof(ntag)); + if (err) { + return err; + } + + if (!(tag & 0x80000000)) { + // from memory + err = lfs2_dir_commitprog(lfs2, commit, buffer, dsize-sizeof(tag)); + if (err) { + return err; + } + } else { + // from disk + const struct lfs2_diskoff *disk = buffer; + for (lfs2_off_t i = 0; i < dsize-sizeof(tag); i++) { + // rely on caching to make this efficient + uint8_t dat; + err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, dsize-sizeof(tag)-i, + disk->block, disk->off+i, &dat, 1); + if (err) { + return err; + } + + err = lfs2_dir_commitprog(lfs2, commit, &dat, 1); + if (err) { + return err; + } + } + } + + commit->ptag = tag & 0x7fffffff; + return 0; +} + +static int lfs2_dir_commitcrc(lfs2_t *lfs2, struct lfs2_commit *commit) { + const lfs2_off_t off1 = commit->off; + const uint32_t crc1 = commit->crc; + // align to program units + const lfs2_off_t end = lfs2_alignup(off1 + 2*sizeof(uint32_t), + lfs2->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) { + lfs2_off_t off = commit->off + sizeof(lfs2_tag_t); + lfs2_off_t noff = lfs2_min(end - off, 0x3fe) + off; + if (noff < end) { + noff = lfs2_min(noff, end - 2*sizeof(uint32_t)); + } + + // read erased state from next program unit + lfs2_tag_t tag = 0xffffffff; + int err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, sizeof(tag), + commit->block, noff, &tag, sizeof(tag)); + if (err && err != LFS2_ERR_CORRUPT) { + return err; + } + + // build crc tag + bool reset = ~lfs2_frombe32(tag) >> 31; + tag = LFS2_MKTAG(LFS2_TYPE_CRC + reset, 0x3ff, noff - off); + + // write out crc + uint32_t footer[2]; + footer[0] = lfs2_tobe32(tag ^ commit->ptag); + commit->crc = lfs2_crc(commit->crc, &footer[0], sizeof(footer[0])); + footer[1] = lfs2_tole32(commit->crc); + err = lfs2_bd_prog(lfs2, + &lfs2->pcache, &lfs2->rcache, false, + commit->block, commit->off, &footer, sizeof(footer)); + if (err) { + return err; + } + + commit->off += sizeof(tag)+lfs2_tag_size(tag); + commit->ptag = tag ^ ((lfs2_tag_t)reset << 31); + commit->crc = 0xffffffff; // reset crc for next "commit" + } + + // flush buffers + int err = lfs2_bd_sync(lfs2, &lfs2->pcache, &lfs2->rcache, false); + if (err) { + return err; + } + + // successful commit, check checksums to make sure + lfs2_off_t off = commit->begin; + lfs2_off_t noff = off1 + sizeof(uint32_t); + while (off < end) { + uint32_t crc = 0xffffffff; + for (lfs2_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 LFS2_ERR_CORRUPT; + } + + // leave it up to caching to make this efficient + uint8_t dat; + err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, noff+sizeof(uint32_t)-i, + commit->block, i, &dat, 1); + if (err) { + return err; + } + + crc = lfs2_crc(crc, &dat, 1); + } + + // detected write error? + if (crc != 0) { + return LFS2_ERR_CORRUPT; + } + + // skip padding + off = lfs2_min(end - noff, 0x3fe) + noff; + if (off < end) { + off = lfs2_min(off, end - 2*sizeof(uint32_t)); + } + noff = off + sizeof(uint32_t); + } + + return 0; +} + +static int lfs2_dir_alloc(lfs2_t *lfs2, lfs2_mdir_t *dir) { + // allocate pair of dir blocks (backwards, so we write block 1 first) + for (int i = 0; i < 2; i++) { + int err = lfs2_alloc(lfs2, &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 = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, sizeof(dir->rev), + dir->pair[0], 0, &dir->rev, sizeof(dir->rev)); + dir->rev = lfs2_fromle32(dir->rev); + if (err && err != LFS2_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] = LFS2_BLOCK_NULL; + dir->tail[1] = LFS2_BLOCK_NULL; + dir->erased = false; + dir->split = false; + + // don't write out yet, let caller take care of that + return 0; +} + +static int lfs2_dir_drop(lfs2_t *lfs2, lfs2_mdir_t *dir, lfs2_mdir_t *tail) { + // steal state + int err = lfs2_dir_getgstate(lfs2, tail, &lfs2->gdelta); + if (err) { + return err; + } + + // steal tail + lfs2_pair_tole32(tail->tail); + err = lfs2_dir_commit(lfs2, dir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_TAIL + tail->split, 0x3ff, 8), tail->tail})); + lfs2_pair_fromle32(tail->tail); + if (err) { + return err; + } + + return 0; +} + +static int lfs2_dir_split(lfs2_t *lfs2, + lfs2_mdir_t *dir, const struct lfs2_mattr *attrs, int attrcount, + lfs2_mdir_t *source, uint16_t split, uint16_t end) { + // create tail directory + lfs2_alloc_ack(lfs2); + lfs2_mdir_t tail; + int err = lfs2_dir_alloc(lfs2, &tail); + if (err) { + return err; + } + + tail.split = dir->split; + tail.tail[0] = dir->tail[0]; + tail.tail[1] = dir->tail[1]; + + err = lfs2_dir_compact(lfs2, &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 (lfs2_pair_cmp(dir->pair, lfs2->root) == 0 && split == 0) { + lfs2->root[0] = tail.pair[0]; + lfs2->root[1] = tail.pair[1]; + } + + return 0; +} + +static int lfs2_dir_commit_size(void *p, lfs2_tag_t tag, const void *buffer) { + lfs2_size_t *size = p; + (void)buffer; + + *size += lfs2_tag_dsize(tag); + return 0; +} + +struct lfs2_dir_commit_commit { + lfs2_t *lfs2; + struct lfs2_commit *commit; +}; + +static int lfs2_dir_commit_commit(void *p, lfs2_tag_t tag, const void *buffer) { + struct lfs2_dir_commit_commit *commit = p; + return lfs2_dir_commitattr(commit->lfs2, commit->commit, tag, buffer); +} + +static int lfs2_dir_compact(lfs2_t *lfs2, + lfs2_mdir_t *dir, const struct lfs2_mattr *attrs, int attrcount, + lfs2_mdir_t *source, uint16_t begin, uint16_t end) { + // save some state in case block is bad + const lfs2_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 + lfs2_size_t size = 0; + int err = lfs2_dir_traverse(lfs2, + source, 0, 0xffffffff, attrs, attrcount, + LFS2_MKTAG(0x400, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_NAME, 0, 0), + begin, end, -begin, + lfs2_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 <= lfs2_min(lfs2->cfg->block_size - 36, + lfs2_alignup(lfs2->cfg->block_size/2, + lfs2->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 = lfs2_dir_split(lfs2, 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 == LFS2_ERR_NOSPC && size <= lfs2->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 (lfs2->cfg->block_cycles > 0 && + (dir->rev % ((lfs2->cfg->block_cycles+1)|1) == 0)) { + if (lfs2_pair_cmp(dir->pair, (const lfs2_block_t[2]){0, 1}) == 0) { + // oh no! we're writing too much to the superblock, + // should we expand? + lfs2_ssize_t res = lfs2_fs_size(lfs2); + if (res < 0) { + return res; + } + + // do we have extra space? littlefs can't reclaim this space + // by itself, so expand cautiously + if ((lfs2_size_t)res < lfs2->cfg->block_count/2) { + LFS2_DEBUG("Expanding superblock at rev %"PRIu32, dir->rev); + int err = lfs2_dir_split(lfs2, dir, attrs, attrcount, + source, begin, end); + if (err && err != LFS2_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 LFS2_MIGRATE + } else if (lfs2->lfs21) { + // 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 + // lfs2_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 lfs2_commit commit = { + .block = dir->pair[1], + .off = 0, + .ptag = 0xffffffff, + .crc = 0xffffffff, + + .begin = 0, + .end = lfs2->cfg->block_size - 8, + }; + + // erase block to write to + int err = lfs2_bd_erase(lfs2, dir->pair[1]); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // write out header + dir->rev = lfs2_tole32(dir->rev); + err = lfs2_dir_commitprog(lfs2, &commit, + &dir->rev, sizeof(dir->rev)); + dir->rev = lfs2_fromle32(dir->rev); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // traverse the directory, this time writing out all unique tags + err = lfs2_dir_traverse(lfs2, + source, 0, 0xffffffff, attrs, attrcount, + LFS2_MKTAG(0x400, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_NAME, 0, 0), + begin, end, -begin, + lfs2_dir_commit_commit, &(struct lfs2_dir_commit_commit){ + lfs2, &commit}); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // commit tail, which may be new after last size check + if (!lfs2_pair_isnull(dir->tail)) { + lfs2_pair_tole32(dir->tail); + err = lfs2_dir_commitattr(lfs2, &commit, + LFS2_MKTAG(LFS2_TYPE_TAIL + dir->split, 0x3ff, 8), + dir->tail); + lfs2_pair_fromle32(dir->tail); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // bring over gstate? + lfs2_gstate_t delta = {0}; + if (!relocated) { + lfs2_gstate_xor(&delta, &lfs2->gdisk); + lfs2_gstate_xor(&delta, &lfs2->gstate); + } + lfs2_gstate_xor(&delta, &lfs2->gdelta); + delta.tag &= ~LFS2_MKTAG(0, 0, 0x3ff); + + err = lfs2_dir_getgstate(lfs2, dir, &delta); + if (err) { + return err; + } + + if (!lfs2_gstate_iszero(&delta)) { + lfs2_gstate_tole32(&delta); + err = lfs2_dir_commitattr(lfs2, &commit, + LFS2_MKTAG(LFS2_TYPE_MOVESTATE, 0x3ff, + sizeof(delta)), &delta); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // complete commit with crc + err = lfs2_dir_commitcrc(lfs2, &commit); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // successful compaction, swap dir pair to indicate most recent + LFS2_ASSERT(commit.off % lfs2->cfg->prog_size == 0); + lfs2_pair_swap(dir->pair); + dir->count = end - begin; + dir->off = commit.off; + dir->etag = commit.ptag; + // update gstate + lfs2->gdelta = (lfs2_gstate_t){0}; + if (!relocated) { + lfs2->gdisk = lfs2->gstate; + } + } + break; + +relocate: + // commit was corrupted, drop caches and prepare to relocate block + relocated = true; + lfs2_cache_drop(lfs2, &lfs2->pcache); + if (!tired) { + LFS2_DEBUG("Bad block at 0x%"PRIx32, dir->pair[1]); + } + + // can't relocate superblock, filesystem is now frozen + if (lfs2_pair_cmp(dir->pair, (const lfs2_block_t[2]){0, 1}) == 0) { + LFS2_WARN("Superblock 0x%"PRIx32" has become unwritable", + dir->pair[1]); + return LFS2_ERR_NOSPC; + } + + // relocate half of pair + int err = lfs2_alloc(lfs2, &dir->pair[1]); + if (err && (err != LFS2_ERR_NOSPC || !tired)) { + return err; + } + + tired = false; + continue; + } + + if (relocated) { + // update references if we relocated + LFS2_DEBUG("Relocating {0x%"PRIx32", 0x%"PRIx32"} " + "-> {0x%"PRIx32", 0x%"PRIx32"}", + oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]); + int err = lfs2_fs_relocate(lfs2, oldpair, dir->pair); + if (err) { + return err; + } + } + + return 0; +} + +static int lfs2_dir_commit(lfs2_t *lfs2, lfs2_mdir_t *dir, + const struct lfs2_mattr *attrs, int attrcount) { + // check for any inline files that aren't RAM backed and + // forcefully evict them, needed for filesystem consistency + for (lfs2_file_t *f = (lfs2_file_t*)lfs2->mlist; f; f = f->next) { + if (dir != &f->m && lfs2_pair_cmp(f->m.pair, dir->pair) == 0 && + f->type == LFS2_TYPE_REG && (f->flags & LFS2_F_INLINE) && + f->ctz.size > lfs2->cfg->cache_size) { + int err = lfs2_file_outline(lfs2, f); + if (err) { + return err; + } + + err = lfs2_file_flush(lfs2, f); + if (err) { + return err; + } + } + } + + // calculate changes to the directory + lfs2_mdir_t olddir = *dir; + bool hasdelete = false; + for (int i = 0; i < attrcount; i++) { + if (lfs2_tag_type3(attrs[i].tag) == LFS2_TYPE_CREATE) { + dir->count += 1; + } else if (lfs2_tag_type3(attrs[i].tag) == LFS2_TYPE_DELETE) { + LFS2_ASSERT(dir->count > 0); + dir->count -= 1; + hasdelete = true; + } else if (lfs2_tag_type1(attrs[i].tag) == LFS2_TYPE_TAIL) { + dir->tail[0] = ((lfs2_block_t*)attrs[i].buffer)[0]; + dir->tail[1] = ((lfs2_block_t*)attrs[i].buffer)[1]; + dir->split = (lfs2_tag_chunk(attrs[i].tag) & 1); + lfs2_pair_fromle32(dir->tail); + } + } + + // should we actually drop the directory block? + if (hasdelete && dir->count == 0) { + lfs2_mdir_t pdir; + int err = lfs2_fs_pred(lfs2, dir->pair, &pdir); + if (err && err != LFS2_ERR_NOENT) { + *dir = olddir; + return err; + } + + if (err != LFS2_ERR_NOENT && pdir.split) { + err = lfs2_dir_drop(lfs2, &pdir, dir); + if (err) { + *dir = olddir; + return err; + } + } + } + + if (dir->erased || dir->count >= 0xff) { + // try to commit + struct lfs2_commit commit = { + .block = dir->pair[0], + .off = dir->off, + .ptag = dir->etag, + .crc = 0xffffffff, + + .begin = dir->off, + .end = lfs2->cfg->block_size - 8, + }; + + // traverse attrs that need to be written out + lfs2_pair_tole32(dir->tail); + int err = lfs2_dir_traverse(lfs2, + dir, dir->off, dir->etag, attrs, attrcount, + 0, 0, 0, 0, 0, + lfs2_dir_commit_commit, &(struct lfs2_dir_commit_commit){ + lfs2, &commit}); + lfs2_pair_fromle32(dir->tail); + if (err) { + if (err == LFS2_ERR_NOSPC || err == LFS2_ERR_CORRUPT) { + goto compact; + } + *dir = olddir; + return err; + } + + // commit any global diffs if we have any + lfs2_gstate_t delta = {0}; + lfs2_gstate_xor(&delta, &lfs2->gstate); + lfs2_gstate_xor(&delta, &lfs2->gdisk); + lfs2_gstate_xor(&delta, &lfs2->gdelta); + delta.tag &= ~LFS2_MKTAG(0, 0, 0x3ff); + if (!lfs2_gstate_iszero(&delta)) { + err = lfs2_dir_getgstate(lfs2, dir, &delta); + if (err) { + *dir = olddir; + return err; + } + + lfs2_gstate_tole32(&delta); + err = lfs2_dir_commitattr(lfs2, &commit, + LFS2_MKTAG(LFS2_TYPE_MOVESTATE, 0x3ff, + sizeof(delta)), &delta); + if (err) { + if (err == LFS2_ERR_NOSPC || err == LFS2_ERR_CORRUPT) { + goto compact; + } + *dir = olddir; + return err; + } + } + + // finalize commit with the crc + err = lfs2_dir_commitcrc(lfs2, &commit); + if (err) { + if (err == LFS2_ERR_NOSPC || err == LFS2_ERR_CORRUPT) { + goto compact; + } + *dir = olddir; + return err; + } + + // successful commit, update dir + LFS2_ASSERT(commit.off % lfs2->cfg->prog_size == 0); + dir->off = commit.off; + dir->etag = commit.ptag; + // and update gstate + lfs2->gdisk = lfs2->gstate; + lfs2->gdelta = (lfs2_gstate_t){0}; + } else { +compact: + // fall back to compaction + lfs2_cache_drop(lfs2, &lfs2->pcache); + + int err = lfs2_dir_compact(lfs2, 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 + // lfs2_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 lfs2_mlist *d = lfs2->mlist; d; d = d->next) { + if (&d->m != dir && lfs2_pair_cmp(d->m.pair, olddir.pair) == 0) { + d->m = *dir; + for (int i = 0; i < attrcount; i++) { + if (lfs2_tag_type3(attrs[i].tag) == LFS2_TYPE_DELETE && + d->id == lfs2_tag_id(attrs[i].tag)) { + d->m.pair[0] = LFS2_BLOCK_NULL; + d->m.pair[1] = LFS2_BLOCK_NULL; + } else if (lfs2_tag_type3(attrs[i].tag) == LFS2_TYPE_DELETE && + d->id > lfs2_tag_id(attrs[i].tag)) { + d->id -= 1; + if (d->type == LFS2_TYPE_DIR) { + ((lfs2_dir_t*)d)->pos -= 1; + } + } else if (lfs2_tag_type3(attrs[i].tag) == LFS2_TYPE_CREATE && + d->id >= lfs2_tag_id(attrs[i].tag)) { + d->id += 1; + if (d->type == LFS2_TYPE_DIR) { + ((lfs2_dir_t*)d)->pos += 1; + } + } + } + } + } + + for (struct lfs2_mlist *d = lfs2->mlist; d; d = d->next) { + if (lfs2_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 = lfs2_dir_fetch(lfs2, &d->m, d->m.tail); + if (err) { + return err; + } + } + } + } + + return 0; +} + + +/// Top level directory operations /// +int lfs2_mkdir(lfs2_t *lfs2, const char *path) { + LFS2_TRACE("lfs2_mkdir(%p, \"%s\")", (void*)lfs2, path); + // deorphan if we haven't yet, needed at most once after poweron + int err = lfs2_fs_forceconsistency(lfs2); + if (err) { + LFS2_TRACE("lfs2_mkdir -> %d", err); + return err; + } + + struct lfs2_mlist cwd; + cwd.next = lfs2->mlist; + uint16_t id; + err = lfs2_dir_find(lfs2, &cwd.m, &path, &id); + if (!(err == LFS2_ERR_NOENT && id != 0x3ff)) { + LFS2_TRACE("lfs2_mkdir -> %d", (err < 0) ? err : LFS2_ERR_EXIST); + return (err < 0) ? err : LFS2_ERR_EXIST; + } + + // check that name fits + lfs2_size_t nlen = strlen(path); + if (nlen > lfs2->name_max) { + LFS2_TRACE("lfs2_mkdir -> %d", LFS2_ERR_NAMETOOLONG); + return LFS2_ERR_NAMETOOLONG; + } + + // build up new directory + lfs2_alloc_ack(lfs2); + lfs2_mdir_t dir; + err = lfs2_dir_alloc(lfs2, &dir); + if (err) { + LFS2_TRACE("lfs2_mkdir -> %d", err); + return err; + } + + // find end of list + lfs2_mdir_t pred = cwd.m; + while (pred.split) { + err = lfs2_dir_fetch(lfs2, &pred, pred.tail); + if (err) { + LFS2_TRACE("lfs2_mkdir -> %d", err); + return err; + } + } + + // setup dir + lfs2_pair_tole32(pred.tail); + err = lfs2_dir_commit(lfs2, &dir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_SOFTTAIL, 0x3ff, 8), pred.tail})); + lfs2_pair_fromle32(pred.tail); + if (err) { + LFS2_TRACE("lfs2_mkdir -> %d", err); + return err; + } + + // current block end of list? + if (cwd.m.split) { + // update tails, this creates a desync + lfs2_fs_preporphans(lfs2, +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; + lfs2->mlist = &cwd; + + lfs2_pair_tole32(dir.pair); + err = lfs2_dir_commit(lfs2, &pred, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair})); + lfs2_pair_fromle32(dir.pair); + if (err) { + lfs2->mlist = cwd.next; + LFS2_TRACE("lfs2_mkdir -> %d", err); + return err; + } + + lfs2->mlist = cwd.next; + lfs2_fs_preporphans(lfs2, -1); + } + + // now insert into our parent block + lfs2_pair_tole32(dir.pair); + err = lfs2_dir_commit(lfs2, &cwd.m, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_CREATE, id, 0), NULL}, + {LFS2_MKTAG(LFS2_TYPE_DIR, id, nlen), path}, + {LFS2_MKTAG(LFS2_TYPE_DIRSTRUCT, id, 8), dir.pair}, + {LFS2_MKTAG_IF(!cwd.m.split, + LFS2_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair})); + lfs2_pair_fromle32(dir.pair); + if (err) { + LFS2_TRACE("lfs2_mkdir -> %d", err); + return err; + } + + LFS2_TRACE("lfs2_mkdir -> %d", 0); + return 0; +} + +int lfs2_dir_open(lfs2_t *lfs2, lfs2_dir_t *dir, const char *path) { + LFS2_TRACE("lfs2_dir_open(%p, %p, \"%s\")", (void*)lfs2, (void*)dir, path); + lfs2_stag_t tag = lfs2_dir_find(lfs2, &dir->m, &path, NULL); + if (tag < 0) { + LFS2_TRACE("lfs2_dir_open -> %"PRId32, tag); + return tag; + } + + if (lfs2_tag_type3(tag) != LFS2_TYPE_DIR) { + LFS2_TRACE("lfs2_dir_open -> %d", LFS2_ERR_NOTDIR); + return LFS2_ERR_NOTDIR; + } + + lfs2_block_t pair[2]; + if (lfs2_tag_id(tag) == 0x3ff) { + // handle root dir separately + pair[0] = lfs2->root[0]; + pair[1] = lfs2->root[1]; + } else { + // get dir pair from parent + lfs2_stag_t res = lfs2_dir_get(lfs2, &dir->m, LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, lfs2_tag_id(tag), 8), pair); + if (res < 0) { + LFS2_TRACE("lfs2_dir_open -> %"PRId32, res); + return res; + } + lfs2_pair_fromle32(pair); + } + + // fetch first pair + int err = lfs2_dir_fetch(lfs2, &dir->m, pair); + if (err) { + LFS2_TRACE("lfs2_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 = LFS2_TYPE_DIR; + dir->next = (lfs2_dir_t*)lfs2->mlist; + lfs2->mlist = (struct lfs2_mlist*)dir; + + LFS2_TRACE("lfs2_dir_open -> %d", 0); + return 0; +} + +int lfs2_dir_close(lfs2_t *lfs2, lfs2_dir_t *dir) { + LFS2_TRACE("lfs2_dir_close(%p, %p)", (void*)lfs2, (void*)dir); + // remove from list of mdirs + for (struct lfs2_mlist **p = &lfs2->mlist; *p; p = &(*p)->next) { + if (*p == (struct lfs2_mlist*)dir) { + *p = (*p)->next; + break; + } + } + + LFS2_TRACE("lfs2_dir_close -> %d", 0); + return 0; +} + +int lfs2_dir_read(lfs2_t *lfs2, lfs2_dir_t *dir, struct lfs2_info *info) { + LFS2_TRACE("lfs2_dir_read(%p, %p, %p)", + (void*)lfs2, (void*)dir, (void*)info); + memset(info, 0, sizeof(*info)); + + // special offset for '.' and '..' + if (dir->pos == 0) { + info->type = LFS2_TYPE_DIR; + strcpy(info->name, "."); + dir->pos += 1; + LFS2_TRACE("lfs2_dir_read -> %d", true); + return true; + } else if (dir->pos == 1) { + info->type = LFS2_TYPE_DIR; + strcpy(info->name, ".."); + dir->pos += 1; + LFS2_TRACE("lfs2_dir_read -> %d", true); + return true; + } + + while (true) { + if (dir->id == dir->m.count) { + if (!dir->m.split) { + LFS2_TRACE("lfs2_dir_read -> %d", false); + return false; + } + + int err = lfs2_dir_fetch(lfs2, &dir->m, dir->m.tail); + if (err) { + LFS2_TRACE("lfs2_dir_read -> %d", err); + return err; + } + + dir->id = 0; + } + + int err = lfs2_dir_getinfo(lfs2, &dir->m, dir->id, info); + if (err && err != LFS2_ERR_NOENT) { + LFS2_TRACE("lfs2_dir_read -> %d", err); + return err; + } + + dir->id += 1; + if (err != LFS2_ERR_NOENT) { + break; + } + } + + dir->pos += 1; + LFS2_TRACE("lfs2_dir_read -> %d", true); + return true; +} + +int lfs2_dir_seek(lfs2_t *lfs2, lfs2_dir_t *dir, lfs2_off_t off) { + LFS2_TRACE("lfs2_dir_seek(%p, %p, %"PRIu32")", + (void*)lfs2, (void*)dir, off); + // simply walk from head dir + int err = lfs2_dir_rewind(lfs2, dir); + if (err) { + LFS2_TRACE("lfs2_dir_seek -> %d", err); + return err; + } + + // first two for ./.. + dir->pos = lfs2_min(2, off); + off -= dir->pos; + + // skip superblock entry + dir->id = (off > 0 && lfs2_pair_cmp(dir->head, lfs2->root) == 0); + + while (off > 0) { + int diff = lfs2_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) { + LFS2_TRACE("lfs2_dir_seek -> %d", LFS2_ERR_INVAL); + return LFS2_ERR_INVAL; + } + + err = lfs2_dir_fetch(lfs2, &dir->m, dir->m.tail); + if (err) { + LFS2_TRACE("lfs2_dir_seek -> %d", err); + return err; + } + + dir->id = 0; + } + } + + LFS2_TRACE("lfs2_dir_seek -> %d", 0); + return 0; +} + +lfs2_soff_t lfs2_dir_tell(lfs2_t *lfs2, lfs2_dir_t *dir) { + LFS2_TRACE("lfs2_dir_tell(%p, %p)", (void*)lfs2, (void*)dir); + (void)lfs2; + LFS2_TRACE("lfs2_dir_tell -> %"PRId32, dir->pos); + return dir->pos; +} + +int lfs2_dir_rewind(lfs2_t *lfs2, lfs2_dir_t *dir) { + LFS2_TRACE("lfs2_dir_rewind(%p, %p)", (void*)lfs2, (void*)dir); + // reload the head dir + int err = lfs2_dir_fetch(lfs2, &dir->m, dir->head); + if (err) { + LFS2_TRACE("lfs2_dir_rewind -> %d", err); + return err; + } + + dir->id = 0; + dir->pos = 0; + LFS2_TRACE("lfs2_dir_rewind -> %d", 0); + return 0; +} + + +/// File index list operations /// +static int lfs2_ctz_index(lfs2_t *lfs2, lfs2_off_t *off) { + lfs2_off_t size = *off; + lfs2_off_t b = lfs2->cfg->block_size - 2*4; + lfs2_off_t i = size / b; + if (i == 0) { + return 0; + } + + i = (size - 4*(lfs2_popc(i-1)+2)) / b; + *off = size - b*i - 4*lfs2_popc(i); + return i; +} + +static int lfs2_ctz_find(lfs2_t *lfs2, + const lfs2_cache_t *pcache, lfs2_cache_t *rcache, + lfs2_block_t head, lfs2_size_t size, + lfs2_size_t pos, lfs2_block_t *block, lfs2_off_t *off) { + if (size == 0) { + *block = LFS2_BLOCK_NULL; + *off = 0; + return 0; + } + + lfs2_off_t current = lfs2_ctz_index(lfs2, &(lfs2_off_t){size-1}); + lfs2_off_t target = lfs2_ctz_index(lfs2, &pos); + + while (current > target) { + lfs2_size_t skip = lfs2_min( + lfs2_npw2(current-target+1) - 1, + lfs2_ctz(current)); + + int err = lfs2_bd_read(lfs2, + pcache, rcache, sizeof(head), + head, 4*skip, &head, sizeof(head)); + head = lfs2_fromle32(head); + if (err) { + return err; + } + + current -= 1 << skip; + } + + *block = head; + *off = pos; + return 0; +} + +static int lfs2_ctz_extend(lfs2_t *lfs2, + lfs2_cache_t *pcache, lfs2_cache_t *rcache, + lfs2_block_t head, lfs2_size_t size, + lfs2_block_t *block, lfs2_off_t *off) { + while (true) { + // go ahead and grab a block + lfs2_block_t nblock; + int err = lfs2_alloc(lfs2, &nblock); + if (err) { + return err; + } + + { + err = lfs2_bd_erase(lfs2, nblock); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (size == 0) { + *block = nblock; + *off = 0; + return 0; + } + + lfs2_size_t noff = size - 1; + lfs2_off_t index = lfs2_ctz_index(lfs2, &noff); + noff = noff + 1; + + // just copy out the last block if it is incomplete + if (noff != lfs2->cfg->block_size) { + for (lfs2_off_t i = 0; i < noff; i++) { + uint8_t data; + err = lfs2_bd_read(lfs2, + NULL, rcache, noff-i, + head, i, &data, 1); + if (err) { + return err; + } + + err = lfs2_bd_prog(lfs2, + pcache, rcache, true, + nblock, i, &data, 1); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + *block = nblock; + *off = noff; + return 0; + } + + // append block + index += 1; + lfs2_size_t skips = lfs2_ctz(index) + 1; + lfs2_block_t nhead = head; + for (lfs2_off_t i = 0; i < skips; i++) { + nhead = lfs2_tole32(nhead); + err = lfs2_bd_prog(lfs2, pcache, rcache, true, + nblock, 4*i, &nhead, 4); + nhead = lfs2_fromle32(nhead); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (i != skips-1) { + err = lfs2_bd_read(lfs2, + NULL, rcache, sizeof(nhead), + nhead, 4*i, &nhead, sizeof(nhead)); + nhead = lfs2_fromle32(nhead); + if (err) { + return err; + } + } + } + + *block = nblock; + *off = 4*skips; + return 0; + } + +relocate: + LFS2_DEBUG("Bad block at 0x%"PRIx32, nblock); + + // just clear cache and try a new block + lfs2_cache_drop(lfs2, pcache); + } +} + +static int lfs2_ctz_traverse(lfs2_t *lfs2, + const lfs2_cache_t *pcache, lfs2_cache_t *rcache, + lfs2_block_t head, lfs2_size_t size, + int (*cb)(void*, lfs2_block_t), void *data) { + if (size == 0) { + return 0; + } + + lfs2_off_t index = lfs2_ctz_index(lfs2, &(lfs2_off_t){size-1}); + + while (true) { + int err = cb(data, head); + if (err) { + return err; + } + + if (index == 0) { + return 0; + } + + lfs2_block_t heads[2]; + int count = 2 - (index & 1); + err = lfs2_bd_read(lfs2, + pcache, rcache, count*sizeof(head), + head, 0, &heads, count*sizeof(head)); + heads[0] = lfs2_fromle32(heads[0]); + heads[1] = lfs2_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 lfs2_file_opencfg(lfs2_t *lfs2, lfs2_file_t *file, + const char *path, int flags, + const struct lfs2_file_config *cfg) { + LFS2_TRACE("lfs2_file_opencfg(%p, %p, \"%s\", %x, %p {" + ".buffer=%p, .attrs=%p, .attr_count=%"PRIu32"})", + (void*)lfs2, (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) != LFS2_O_RDONLY) { + int err = lfs2_fs_forceconsistency(lfs2); + if (err) { + LFS2_TRACE("lfs2_file_opencfg -> %d", err); + return err; + } + } + + // setup simple file details + int err; + file->cfg = cfg; + file->flags = flags | LFS2_F_OPENED; + file->pos = 0; + file->off = 0; + file->cache.buffer = NULL; + + // allocate entry for file if it doesn't exist + lfs2_stag_t tag = lfs2_dir_find(lfs2, &file->m, &path, &file->id); + if (tag < 0 && !(tag == LFS2_ERR_NOENT && file->id != 0x3ff)) { + err = tag; + goto cleanup; + } + + // get id, add to list of mdirs to catch update changes + file->type = LFS2_TYPE_REG; + file->next = (lfs2_file_t*)lfs2->mlist; + lfs2->mlist = (struct lfs2_mlist*)file; + + if (tag == LFS2_ERR_NOENT) { + if (!(flags & LFS2_O_CREAT)) { + err = LFS2_ERR_NOENT; + goto cleanup; + } + + // check that name fits + lfs2_size_t nlen = strlen(path); + if (nlen > lfs2->name_max) { + err = LFS2_ERR_NAMETOOLONG; + goto cleanup; + } + + // get next slot and create entry to remember name + err = lfs2_dir_commit(lfs2, &file->m, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_CREATE, file->id, 0)}, + {LFS2_MKTAG(LFS2_TYPE_REG, file->id, nlen), path}, + {LFS2_MKTAG(LFS2_TYPE_INLINESTRUCT, file->id, 0)})); + if (err) { + err = LFS2_ERR_NAMETOOLONG; + goto cleanup; + } + + tag = LFS2_MKTAG(LFS2_TYPE_INLINESTRUCT, 0, 0); + } else if (flags & LFS2_O_EXCL) { + err = LFS2_ERR_EXIST; + goto cleanup; + } else if (lfs2_tag_type3(tag) != LFS2_TYPE_REG) { + err = LFS2_ERR_ISDIR; + goto cleanup; + } else if (flags & LFS2_O_TRUNC) { + // truncate if requested + tag = LFS2_MKTAG(LFS2_TYPE_INLINESTRUCT, file->id, 0); + file->flags |= LFS2_F_DIRTY; + } else { + // try to load what's on disk, if it's inlined we'll fix it later + tag = lfs2_dir_get(lfs2, &file->m, LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, file->id, 8), &file->ctz); + if (tag < 0) { + err = tag; + goto cleanup; + } + lfs2_ctz_fromle32(&file->ctz); + } + + // fetch attrs + for (unsigned i = 0; i < file->cfg->attr_count; i++) { + if ((file->flags & 3) != LFS2_O_WRONLY) { + lfs2_stag_t res = lfs2_dir_get(lfs2, &file->m, + LFS2_MKTAG(0x7ff, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_USERATTR + file->cfg->attrs[i].type, + file->id, file->cfg->attrs[i].size), + file->cfg->attrs[i].buffer); + if (res < 0 && res != LFS2_ERR_NOENT) { + err = res; + goto cleanup; + } + } + + if ((file->flags & 3) != LFS2_O_RDONLY) { + if (file->cfg->attrs[i].size > lfs2->attr_max) { + err = LFS2_ERR_NOSPC; + goto cleanup; + } + + file->flags |= LFS2_F_DIRTY; + } + } + + // allocate buffer if needed + if (file->cfg->buffer) { + file->cache.buffer = file->cfg->buffer; + } else { + file->cache.buffer = lfs2_malloc(lfs2->cfg->cache_size); + if (!file->cache.buffer) { + err = LFS2_ERR_NOMEM; + goto cleanup; + } + } + + // zero to avoid information leak + lfs2_cache_zero(lfs2, &file->cache); + + if (lfs2_tag_type3(tag) == LFS2_TYPE_INLINESTRUCT) { + // load inline files + file->ctz.head = LFS2_BLOCK_INLINE; + file->ctz.size = lfs2_tag_size(tag); + file->flags |= LFS2_F_INLINE; + file->cache.block = file->ctz.head; + file->cache.off = 0; + file->cache.size = lfs2->cfg->cache_size; + + // don't always read (may be new/trunc file) + if (file->ctz.size > 0) { + lfs2_stag_t res = lfs2_dir_get(lfs2, &file->m, + LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, file->id, + lfs2_min(file->cache.size, 0x3fe)), + file->cache.buffer); + if (res < 0) { + err = res; + goto cleanup; + } + } + } + + LFS2_TRACE("lfs2_file_opencfg -> %d", 0); + return 0; + +cleanup: + // clean up lingering resources + file->flags |= LFS2_F_ERRED; + lfs2_file_close(lfs2, file); + LFS2_TRACE("lfs2_file_opencfg -> %d", err); + return err; +} + +int lfs2_file_open(lfs2_t *lfs2, lfs2_file_t *file, + const char *path, int flags) { + LFS2_TRACE("lfs2_file_open(%p, %p, \"%s\", %x)", + (void*)lfs2, (void*)file, path, flags); + static const struct lfs2_file_config defaults = {0}; + int err = lfs2_file_opencfg(lfs2, file, path, flags, &defaults); + LFS2_TRACE("lfs2_file_open -> %d", err); + return err; +} + +int lfs2_file_close(lfs2_t *lfs2, lfs2_file_t *file) { + LFS2_TRACE("lfs2_file_close(%p, %p)", (void*)lfs2, (void*)file); + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + + int err = lfs2_file_sync(lfs2, file); + + // remove from list of mdirs + for (struct lfs2_mlist **p = &lfs2->mlist; *p; p = &(*p)->next) { + if (*p == (struct lfs2_mlist*)file) { + *p = (*p)->next; + break; + } + } + + // clean up memory + if (!file->cfg->buffer) { + lfs2_free(file->cache.buffer); + } + + file->flags &= ~LFS2_F_OPENED; + LFS2_TRACE("lfs2_file_close -> %d", err); + return err; +} + +static int lfs2_file_relocate(lfs2_t *lfs2, lfs2_file_t *file) { + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + + while (true) { + // just relocate what exists into new block + lfs2_block_t nblock; + int err = lfs2_alloc(lfs2, &nblock); + if (err) { + return err; + } + + err = lfs2_bd_erase(lfs2, nblock); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // either read from dirty cache or disk + for (lfs2_off_t i = 0; i < file->off; i++) { + uint8_t data; + if (file->flags & LFS2_F_INLINE) { + err = lfs2_dir_getread(lfs2, &file->m, + // note we evict inline files before they can be dirty + NULL, &file->cache, file->off-i, + LFS2_MKTAG(0xfff, 0x1ff, 0), + LFS2_MKTAG(LFS2_TYPE_INLINESTRUCT, file->id, 0), + i, &data, 1); + if (err) { + return err; + } + } else { + err = lfs2_bd_read(lfs2, + &file->cache, &lfs2->rcache, file->off-i, + file->block, i, &data, 1); + if (err) { + return err; + } + } + + err = lfs2_bd_prog(lfs2, + &lfs2->pcache, &lfs2->rcache, true, + nblock, i, &data, 1); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // copy over new state of file + memcpy(file->cache.buffer, lfs2->pcache.buffer, lfs2->cfg->cache_size); + file->cache.block = lfs2->pcache.block; + file->cache.off = lfs2->pcache.off; + file->cache.size = lfs2->pcache.size; + lfs2_cache_zero(lfs2, &lfs2->pcache); + + file->block = nblock; + file->flags |= LFS2_F_WRITING; + return 0; + +relocate: + LFS2_DEBUG("Bad block at 0x%"PRIx32, nblock); + + // just clear cache and try a new block + lfs2_cache_drop(lfs2, &lfs2->pcache); + } +} + +static int lfs2_file_outline(lfs2_t *lfs2, lfs2_file_t *file) { + file->off = file->pos; + lfs2_alloc_ack(lfs2); + int err = lfs2_file_relocate(lfs2, file); + if (err) { + return err; + } + + file->flags &= ~LFS2_F_INLINE; + return 0; +} + +static int lfs2_file_flush(lfs2_t *lfs2, lfs2_file_t *file) { + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + + if (file->flags & LFS2_F_READING) { + if (!(file->flags & LFS2_F_INLINE)) { + lfs2_cache_drop(lfs2, &file->cache); + } + file->flags &= ~LFS2_F_READING; + } + + if (file->flags & LFS2_F_WRITING) { + lfs2_off_t pos = file->pos; + + if (!(file->flags & LFS2_F_INLINE)) { + // copy over anything after current branch + lfs2_file_t orig = { + .ctz.head = file->ctz.head, + .ctz.size = file->ctz.size, + .flags = LFS2_O_RDONLY | LFS2_F_OPENED, + .pos = file->pos, + .cache = lfs2->rcache, + }; + lfs2_cache_drop(lfs2, &lfs2->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; + lfs2_ssize_t res = lfs2_file_read(lfs2, &orig, &data, 1); + if (res < 0) { + return res; + } + + res = lfs2_file_write(lfs2, file, &data, 1); + if (res < 0) { + return res; + } + + // keep our reference to the rcache in sync + if (lfs2->rcache.block != LFS2_BLOCK_NULL) { + lfs2_cache_drop(lfs2, &orig.cache); + lfs2_cache_drop(lfs2, &lfs2->rcache); + } + } + + // write out what we have + while (true) { + int err = lfs2_bd_flush(lfs2, &file->cache, &lfs2->rcache, true); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + break; + +relocate: + LFS2_DEBUG("Bad block at 0x%"PRIx32, file->block); + err = lfs2_file_relocate(lfs2, file); + if (err) { + return err; + } + } + } else { + file->pos = lfs2_max(file->pos, file->ctz.size); + } + + // actual file updates + file->ctz.head = file->block; + file->ctz.size = file->pos; + file->flags &= ~LFS2_F_WRITING; + file->flags |= LFS2_F_DIRTY; + + file->pos = pos; + } + + return 0; +} + +int lfs2_file_sync(lfs2_t *lfs2, lfs2_file_t *file) { + LFS2_TRACE("lfs2_file_sync(%p, %p)", (void*)lfs2, (void*)file); + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + + if (file->flags & LFS2_F_ERRED) { + // it's not safe to do anything if our file errored + LFS2_TRACE("lfs2_file_sync -> %d", 0); + return 0; + } + + int err = lfs2_file_flush(lfs2, file); + if (err) { + file->flags |= LFS2_F_ERRED; + LFS2_TRACE("lfs2_file_sync -> %d", err); + return err; + } + + if ((file->flags & LFS2_F_DIRTY) && + !lfs2_pair_isnull(file->m.pair)) { + // update dir entry + uint16_t type; + const void *buffer; + lfs2_size_t size; + struct lfs2_ctz ctz; + if (file->flags & LFS2_F_INLINE) { + // inline the whole file + type = LFS2_TYPE_INLINESTRUCT; + buffer = file->cache.buffer; + size = file->ctz.size; + } else { + // update the ctz reference + type = LFS2_TYPE_CTZSTRUCT; + // copy ctz so alloc will work during a relocate + ctz = file->ctz; + lfs2_ctz_tole32(&ctz); + buffer = &ctz; + size = sizeof(ctz); + } + + // commit file data and attributes + err = lfs2_dir_commit(lfs2, &file->m, LFS2_MKATTRS( + {LFS2_MKTAG(type, file->id, size), buffer}, + {LFS2_MKTAG(LFS2_FROM_USERATTRS, file->id, + file->cfg->attr_count), file->cfg->attrs})); + if (err) { + file->flags |= LFS2_F_ERRED; + LFS2_TRACE("lfs2_file_sync -> %d", err); + return err; + } + + file->flags &= ~LFS2_F_DIRTY; + } + + LFS2_TRACE("lfs2_file_sync -> %d", 0); + return 0; +} + +lfs2_ssize_t lfs2_file_read(lfs2_t *lfs2, lfs2_file_t *file, + void *buffer, lfs2_size_t size) { + LFS2_TRACE("lfs2_file_read(%p, %p, %p, %"PRIu32")", + (void*)lfs2, (void*)file, buffer, size); + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + LFS2_ASSERT((file->flags & 3) != LFS2_O_WRONLY); + + uint8_t *data = buffer; + lfs2_size_t nsize = size; + + if (file->flags & LFS2_F_WRITING) { + // flush out any writes + int err = lfs2_file_flush(lfs2, file); + if (err) { + LFS2_TRACE("lfs2_file_read -> %d", err); + return err; + } + } + + if (file->pos >= file->ctz.size) { + // eof if past end + LFS2_TRACE("lfs2_file_read -> %d", 0); + return 0; + } + + size = lfs2_min(size, file->ctz.size - file->pos); + nsize = size; + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS2_F_READING) || + file->off == lfs2->cfg->block_size) { + if (!(file->flags & LFS2_F_INLINE)) { + int err = lfs2_ctz_find(lfs2, NULL, &file->cache, + file->ctz.head, file->ctz.size, + file->pos, &file->block, &file->off); + if (err) { + LFS2_TRACE("lfs2_file_read -> %d", err); + return err; + } + } else { + file->block = LFS2_BLOCK_INLINE; + file->off = file->pos; + } + + file->flags |= LFS2_F_READING; + } + + // read as much as we can in current block + lfs2_size_t diff = lfs2_min(nsize, lfs2->cfg->block_size - file->off); + if (file->flags & LFS2_F_INLINE) { + int err = lfs2_dir_getread(lfs2, &file->m, + NULL, &file->cache, lfs2->cfg->block_size, + LFS2_MKTAG(0xfff, 0x1ff, 0), + LFS2_MKTAG(LFS2_TYPE_INLINESTRUCT, file->id, 0), + file->off, data, diff); + if (err) { + LFS2_TRACE("lfs2_file_read -> %d", err); + return err; + } + } else { + int err = lfs2_bd_read(lfs2, + NULL, &file->cache, lfs2->cfg->block_size, + file->block, file->off, data, diff); + if (err) { + LFS2_TRACE("lfs2_file_read -> %d", err); + return err; + } + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + } + + LFS2_TRACE("lfs2_file_read -> %"PRId32, size); + return size; +} + +lfs2_ssize_t lfs2_file_write(lfs2_t *lfs2, lfs2_file_t *file, + const void *buffer, lfs2_size_t size) { + LFS2_TRACE("lfs2_file_write(%p, %p, %p, %"PRIu32")", + (void*)lfs2, (void*)file, buffer, size); + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + LFS2_ASSERT((file->flags & 3) != LFS2_O_RDONLY); + + const uint8_t *data = buffer; + lfs2_size_t nsize = size; + + if (file->flags & LFS2_F_READING) { + // drop any reads + int err = lfs2_file_flush(lfs2, file); + if (err) { + LFS2_TRACE("lfs2_file_write -> %d", err); + return err; + } + } + + if ((file->flags & LFS2_O_APPEND) && file->pos < file->ctz.size) { + file->pos = file->ctz.size; + } + + if (file->pos + size > lfs2->file_max) { + // Larger than file limit? + LFS2_TRACE("lfs2_file_write -> %d", LFS2_ERR_FBIG); + return LFS2_ERR_FBIG; + } + + if (!(file->flags & LFS2_F_WRITING) && file->pos > file->ctz.size) { + // fill with zeros + lfs2_off_t pos = file->pos; + file->pos = file->ctz.size; + + while (file->pos < pos) { + lfs2_ssize_t res = lfs2_file_write(lfs2, file, &(uint8_t){0}, 1); + if (res < 0) { + LFS2_TRACE("lfs2_file_write -> %"PRId32, res); + return res; + } + } + } + + if ((file->flags & LFS2_F_INLINE) && + lfs2_max(file->pos+nsize, file->ctz.size) > + lfs2_min(0x3fe, lfs2_min( + lfs2->cfg->cache_size, lfs2->cfg->block_size/8))) { + // inline file doesn't fit anymore + int err = lfs2_file_outline(lfs2, file); + if (err) { + file->flags |= LFS2_F_ERRED; + LFS2_TRACE("lfs2_file_write -> %d", err); + return err; + } + } + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS2_F_WRITING) || + file->off == lfs2->cfg->block_size) { + if (!(file->flags & LFS2_F_INLINE)) { + if (!(file->flags & LFS2_F_WRITING) && file->pos > 0) { + // find out which block we're extending from + int err = lfs2_ctz_find(lfs2, NULL, &file->cache, + file->ctz.head, file->ctz.size, + file->pos-1, &file->block, &file->off); + if (err) { + file->flags |= LFS2_F_ERRED; + LFS2_TRACE("lfs2_file_write -> %d", err); + return err; + } + + // mark cache as dirty since we may have read data into it + lfs2_cache_zero(lfs2, &file->cache); + } + + // extend file with new blocks + lfs2_alloc_ack(lfs2); + int err = lfs2_ctz_extend(lfs2, &file->cache, &lfs2->rcache, + file->block, file->pos, + &file->block, &file->off); + if (err) { + file->flags |= LFS2_F_ERRED; + LFS2_TRACE("lfs2_file_write -> %d", err); + return err; + } + } else { + file->block = LFS2_BLOCK_INLINE; + file->off = file->pos; + } + + file->flags |= LFS2_F_WRITING; + } + + // program as much as we can in current block + lfs2_size_t diff = lfs2_min(nsize, lfs2->cfg->block_size - file->off); + while (true) { + int err = lfs2_bd_prog(lfs2, &file->cache, &lfs2->rcache, true, + file->block, file->off, data, diff); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + goto relocate; + } + file->flags |= LFS2_F_ERRED; + LFS2_TRACE("lfs2_file_write -> %d", err); + return err; + } + + break; +relocate: + err = lfs2_file_relocate(lfs2, file); + if (err) { + file->flags |= LFS2_F_ERRED; + LFS2_TRACE("lfs2_file_write -> %d", err); + return err; + } + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + + lfs2_alloc_ack(lfs2); + } + + file->flags &= ~LFS2_F_ERRED; + LFS2_TRACE("lfs2_file_write -> %"PRId32, size); + return size; +} + +lfs2_soff_t lfs2_file_seek(lfs2_t *lfs2, lfs2_file_t *file, + lfs2_soff_t off, int whence) { + LFS2_TRACE("lfs2_file_seek(%p, %p, %"PRId32", %d)", + (void*)lfs2, (void*)file, off, whence); + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + + // write out everything beforehand, may be noop if rdonly + int err = lfs2_file_flush(lfs2, file); + if (err) { + LFS2_TRACE("lfs2_file_seek -> %d", err); + return err; + } + + // find new pos + lfs2_off_t npos = file->pos; + if (whence == LFS2_SEEK_SET) { + npos = off; + } else if (whence == LFS2_SEEK_CUR) { + npos = file->pos + off; + } else if (whence == LFS2_SEEK_END) { + npos = file->ctz.size + off; + } + + if (npos > lfs2->file_max) { + // file position out of range + LFS2_TRACE("lfs2_file_seek -> %d", LFS2_ERR_INVAL); + return LFS2_ERR_INVAL; + } + + // update pos + file->pos = npos; + LFS2_TRACE("lfs2_file_seek -> %"PRId32, npos); + return npos; +} + +int lfs2_file_truncate(lfs2_t *lfs2, lfs2_file_t *file, lfs2_off_t size) { + LFS2_TRACE("lfs2_file_truncate(%p, %p, %"PRIu32")", + (void*)lfs2, (void*)file, size); + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + LFS2_ASSERT((file->flags & 3) != LFS2_O_RDONLY); + + if (size > LFS2_FILE_MAX) { + LFS2_TRACE("lfs2_file_truncate -> %d", LFS2_ERR_INVAL); + return LFS2_ERR_INVAL; + } + + lfs2_off_t pos = file->pos; + lfs2_off_t oldsize = lfs2_file_size(lfs2, file); + if (size < oldsize) { + // need to flush since directly changing metadata + int err = lfs2_file_flush(lfs2, file); + if (err) { + LFS2_TRACE("lfs2_file_truncate -> %d", err); + return err; + } + + // lookup new head in ctz skip list + err = lfs2_ctz_find(lfs2, NULL, &file->cache, + file->ctz.head, file->ctz.size, + size, &file->block, &file->off); + if (err) { + LFS2_TRACE("lfs2_file_truncate -> %d", err); + return err; + } + + file->ctz.head = file->block; + file->ctz.size = size; + file->flags |= LFS2_F_DIRTY | LFS2_F_READING; + } else if (size > oldsize) { + // flush+seek if not already at end + if (file->pos != oldsize) { + lfs2_soff_t res = lfs2_file_seek(lfs2, file, 0, LFS2_SEEK_END); + if (res < 0) { + LFS2_TRACE("lfs2_file_truncate -> %"PRId32, res); + return (int)res; + } + } + + // fill with zeros + while (file->pos < size) { + lfs2_ssize_t res = lfs2_file_write(lfs2, file, &(uint8_t){0}, 1); + if (res < 0) { + LFS2_TRACE("lfs2_file_truncate -> %"PRId32, res); + return (int)res; + } + } + } + + // restore pos + lfs2_soff_t res = lfs2_file_seek(lfs2, file, pos, LFS2_SEEK_SET); + if (res < 0) { + LFS2_TRACE("lfs2_file_truncate -> %"PRId32, res); + return (int)res; + } + + LFS2_TRACE("lfs2_file_truncate -> %d", 0); + return 0; +} + +lfs2_soff_t lfs2_file_tell(lfs2_t *lfs2, lfs2_file_t *file) { + LFS2_TRACE("lfs2_file_tell(%p, %p)", (void*)lfs2, (void*)file); + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + (void)lfs2; + LFS2_TRACE("lfs2_file_tell -> %"PRId32, file->pos); + return file->pos; +} + +int lfs2_file_rewind(lfs2_t *lfs2, lfs2_file_t *file) { + LFS2_TRACE("lfs2_file_rewind(%p, %p)", (void*)lfs2, (void*)file); + lfs2_soff_t res = lfs2_file_seek(lfs2, file, 0, LFS2_SEEK_SET); + if (res < 0) { + LFS2_TRACE("lfs2_file_rewind -> %"PRId32, res); + return (int)res; + } + + LFS2_TRACE("lfs2_file_rewind -> %d", 0); + return 0; +} + +lfs2_soff_t lfs2_file_size(lfs2_t *lfs2, lfs2_file_t *file) { + LFS2_TRACE("lfs2_file_size(%p, %p)", (void*)lfs2, (void*)file); + LFS2_ASSERT(file->flags & LFS2_F_OPENED); + (void)lfs2; + if (file->flags & LFS2_F_WRITING) { + LFS2_TRACE("lfs2_file_size -> %"PRId32, + lfs2_max(file->pos, file->ctz.size)); + return lfs2_max(file->pos, file->ctz.size); + } else { + LFS2_TRACE("lfs2_file_size -> %"PRId32, file->ctz.size); + return file->ctz.size; + } +} + + +/// General fs operations /// +int lfs2_stat(lfs2_t *lfs2, const char *path, struct lfs2_info *info) { + LFS2_TRACE("lfs2_stat(%p, \"%s\", %p)", (void*)lfs2, path, (void*)info); + lfs2_mdir_t cwd; + lfs2_stag_t tag = lfs2_dir_find(lfs2, &cwd, &path, NULL); + if (tag < 0) { + LFS2_TRACE("lfs2_stat -> %"PRId32, tag); + return (int)tag; + } + + int err = lfs2_dir_getinfo(lfs2, &cwd, lfs2_tag_id(tag), info); + LFS2_TRACE("lfs2_stat -> %d", err); + return err; +} + +int lfs2_remove(lfs2_t *lfs2, const char *path) { + LFS2_TRACE("lfs2_remove(%p, \"%s\")", (void*)lfs2, path); + // deorphan if we haven't yet, needed at most once after poweron + int err = lfs2_fs_forceconsistency(lfs2); + if (err) { + LFS2_TRACE("lfs2_remove -> %d", err); + return err; + } + + lfs2_mdir_t cwd; + lfs2_stag_t tag = lfs2_dir_find(lfs2, &cwd, &path, NULL); + if (tag < 0 || lfs2_tag_id(tag) == 0x3ff) { + LFS2_TRACE("lfs2_remove -> %"PRId32, (tag < 0) ? tag : LFS2_ERR_INVAL); + return (tag < 0) ? (int)tag : LFS2_ERR_INVAL; + } + + struct lfs2_mlist dir; + dir.next = lfs2->mlist; + if (lfs2_tag_type3(tag) == LFS2_TYPE_DIR) { + // must be empty before removal + lfs2_block_t pair[2]; + lfs2_stag_t res = lfs2_dir_get(lfs2, &cwd, LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, lfs2_tag_id(tag), 8), pair); + if (res < 0) { + LFS2_TRACE("lfs2_remove -> %"PRId32, res); + return (int)res; + } + lfs2_pair_fromle32(pair); + + err = lfs2_dir_fetch(lfs2, &dir.m, pair); + if (err) { + LFS2_TRACE("lfs2_remove -> %d", err); + return err; + } + + if (dir.m.count > 0 || dir.m.split) { + LFS2_TRACE("lfs2_remove -> %d", LFS2_ERR_NOTEMPTY); + return LFS2_ERR_NOTEMPTY; + } + + // mark fs as orphaned + lfs2_fs_preporphans(lfs2, +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; + lfs2->mlist = &dir; + } + + // delete the entry + err = lfs2_dir_commit(lfs2, &cwd, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_DELETE, lfs2_tag_id(tag), 0)})); + if (err) { + lfs2->mlist = dir.next; + LFS2_TRACE("lfs2_remove -> %d", err); + return err; + } + + lfs2->mlist = dir.next; + if (lfs2_tag_type3(tag) == LFS2_TYPE_DIR) { + // fix orphan + lfs2_fs_preporphans(lfs2, -1); + + err = lfs2_fs_pred(lfs2, dir.m.pair, &cwd); + if (err) { + LFS2_TRACE("lfs2_remove -> %d", err); + return err; + } + + err = lfs2_dir_drop(lfs2, &cwd, &dir.m); + if (err) { + LFS2_TRACE("lfs2_remove -> %d", err); + return err; + } + } + + LFS2_TRACE("lfs2_remove -> %d", 0); + return 0; +} + +int lfs2_rename(lfs2_t *lfs2, const char *oldpath, const char *newpath) { + LFS2_TRACE("lfs2_rename(%p, \"%s\", \"%s\")", (void*)lfs2, oldpath, newpath); + + // deorphan if we haven't yet, needed at most once after poweron + int err = lfs2_fs_forceconsistency(lfs2); + if (err) { + LFS2_TRACE("lfs2_rename -> %d", err); + return err; + } + + // find old entry + lfs2_mdir_t oldcwd; + lfs2_stag_t oldtag = lfs2_dir_find(lfs2, &oldcwd, &oldpath, NULL); + if (oldtag < 0 || lfs2_tag_id(oldtag) == 0x3ff) { + LFS2_TRACE("lfs2_rename -> %"PRId32, + (oldtag < 0) ? oldtag : LFS2_ERR_INVAL); + return (oldtag < 0) ? (int)oldtag : LFS2_ERR_INVAL; + } + + // find new entry + lfs2_mdir_t newcwd; + uint16_t newid; + lfs2_stag_t prevtag = lfs2_dir_find(lfs2, &newcwd, &newpath, &newid); + if ((prevtag < 0 || lfs2_tag_id(prevtag) == 0x3ff) && + !(prevtag == LFS2_ERR_NOENT && newid != 0x3ff)) { + LFS2_TRACE("lfs2_rename -> %"PRId32, + (prevtag < 0) ? prevtag : LFS2_ERR_INVAL); + return (prevtag < 0) ? (int)prevtag : LFS2_ERR_INVAL; + } + + // if we're in the same pair there's a few special cases... + bool samepair = (lfs2_pair_cmp(oldcwd.pair, newcwd.pair) == 0); + uint16_t newoldid = lfs2_tag_id(oldtag); + + struct lfs2_mlist prevdir; + prevdir.next = lfs2->mlist; + if (prevtag == LFS2_ERR_NOENT) { + // check that name fits + lfs2_size_t nlen = strlen(newpath); + if (nlen > lfs2->name_max) { + LFS2_TRACE("lfs2_rename -> %d", LFS2_ERR_NAMETOOLONG); + return LFS2_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 (lfs2_tag_type3(prevtag) != lfs2_tag_type3(oldtag)) { + LFS2_TRACE("lfs2_rename -> %d", LFS2_ERR_ISDIR); + return LFS2_ERR_ISDIR; + } else if (samepair && newid == newoldid) { + // we're renaming to ourselves?? + LFS2_TRACE("lfs2_rename -> %d", 0); + return 0; + } else if (lfs2_tag_type3(prevtag) == LFS2_TYPE_DIR) { + // must be empty before removal + lfs2_block_t prevpair[2]; + lfs2_stag_t res = lfs2_dir_get(lfs2, &newcwd, LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, newid, 8), prevpair); + if (res < 0) { + LFS2_TRACE("lfs2_rename -> %"PRId32, res); + return (int)res; + } + lfs2_pair_fromle32(prevpair); + + // must be empty before removal + err = lfs2_dir_fetch(lfs2, &prevdir.m, prevpair); + if (err) { + LFS2_TRACE("lfs2_rename -> %d", err); + return err; + } + + if (prevdir.m.count > 0 || prevdir.m.split) { + LFS2_TRACE("lfs2_rename -> %d", LFS2_ERR_NOTEMPTY); + return LFS2_ERR_NOTEMPTY; + } + + // mark fs as orphaned + lfs2_fs_preporphans(lfs2, +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; + lfs2->mlist = &prevdir; + } + + if (!samepair) { + lfs2_fs_prepmove(lfs2, newoldid, oldcwd.pair); + } + + // move over all attributes + err = lfs2_dir_commit(lfs2, &newcwd, LFS2_MKATTRS( + {LFS2_MKTAG_IF(prevtag != LFS2_ERR_NOENT, + LFS2_TYPE_DELETE, newid, 0)}, + {LFS2_MKTAG(LFS2_TYPE_CREATE, newid, 0)}, + {LFS2_MKTAG(lfs2_tag_type3(oldtag), newid, strlen(newpath)), newpath}, + {LFS2_MKTAG(LFS2_FROM_MOVE, newid, lfs2_tag_id(oldtag)), &oldcwd}, + {LFS2_MKTAG_IF(samepair, + LFS2_TYPE_DELETE, newoldid, 0)})); + if (err) { + lfs2->mlist = prevdir.next; + LFS2_TRACE("lfs2_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 && lfs2_gstate_hasmove(&lfs2->gstate)) { + // prep gstate and delete move id + lfs2_fs_prepmove(lfs2, 0x3ff, NULL); + err = lfs2_dir_commit(lfs2, &oldcwd, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_DELETE, lfs2_tag_id(oldtag), 0)})); + if (err) { + lfs2->mlist = prevdir.next; + LFS2_TRACE("lfs2_rename -> %d", err); + return err; + } + } + + lfs2->mlist = prevdir.next; + if (prevtag != LFS2_ERR_NOENT && lfs2_tag_type3(prevtag) == LFS2_TYPE_DIR) { + // fix orphan + lfs2_fs_preporphans(lfs2, -1); + + err = lfs2_fs_pred(lfs2, prevdir.m.pair, &newcwd); + if (err) { + LFS2_TRACE("lfs2_rename -> %d", err); + return err; + } + + err = lfs2_dir_drop(lfs2, &newcwd, &prevdir.m); + if (err) { + LFS2_TRACE("lfs2_rename -> %d", err); + return err; + } + } + + LFS2_TRACE("lfs2_rename -> %d", 0); + return 0; +} + +lfs2_ssize_t lfs2_getattr(lfs2_t *lfs2, const char *path, + uint8_t type, void *buffer, lfs2_size_t size) { + LFS2_TRACE("lfs2_getattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")", + (void*)lfs2, path, type, buffer, size); + lfs2_mdir_t cwd; + lfs2_stag_t tag = lfs2_dir_find(lfs2, &cwd, &path, NULL); + if (tag < 0) { + LFS2_TRACE("lfs2_getattr -> %"PRId32, tag); + return tag; + } + + uint16_t id = lfs2_tag_id(tag); + if (id == 0x3ff) { + // special case for root + id = 0; + int err = lfs2_dir_fetch(lfs2, &cwd, lfs2->root); + if (err) { + LFS2_TRACE("lfs2_getattr -> %d", err); + return err; + } + } + + tag = lfs2_dir_get(lfs2, &cwd, LFS2_MKTAG(0x7ff, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_USERATTR + type, + id, lfs2_min(size, lfs2->attr_max)), + buffer); + if (tag < 0) { + if (tag == LFS2_ERR_NOENT) { + LFS2_TRACE("lfs2_getattr -> %d", LFS2_ERR_NOATTR); + return LFS2_ERR_NOATTR; + } + + LFS2_TRACE("lfs2_getattr -> %"PRId32, tag); + return tag; + } + + size = lfs2_tag_size(tag); + LFS2_TRACE("lfs2_getattr -> %"PRId32, size); + return size; +} + +static int lfs2_commitattr(lfs2_t *lfs2, const char *path, + uint8_t type, const void *buffer, lfs2_size_t size) { + lfs2_mdir_t cwd; + lfs2_stag_t tag = lfs2_dir_find(lfs2, &cwd, &path, NULL); + if (tag < 0) { + return tag; + } + + uint16_t id = lfs2_tag_id(tag); + if (id == 0x3ff) { + // special case for root + id = 0; + int err = lfs2_dir_fetch(lfs2, &cwd, lfs2->root); + if (err) { + return err; + } + } + + return lfs2_dir_commit(lfs2, &cwd, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_USERATTR + type, id, size), buffer})); +} + +int lfs2_setattr(lfs2_t *lfs2, const char *path, + uint8_t type, const void *buffer, lfs2_size_t size) { + LFS2_TRACE("lfs2_setattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")", + (void*)lfs2, path, type, buffer, size); + if (size > lfs2->attr_max) { + LFS2_TRACE("lfs2_setattr -> %d", LFS2_ERR_NOSPC); + return LFS2_ERR_NOSPC; + } + + int err = lfs2_commitattr(lfs2, path, type, buffer, size); + LFS2_TRACE("lfs2_setattr -> %d", err); + return err; +} + +int lfs2_removeattr(lfs2_t *lfs2, const char *path, uint8_t type) { + LFS2_TRACE("lfs2_removeattr(%p, \"%s\", %"PRIu8")", (void*)lfs2, path, type); + int err = lfs2_commitattr(lfs2, path, type, NULL, 0x3ff); + LFS2_TRACE("lfs2_removeattr -> %d", err); + return err; +} + + +/// Filesystem operations /// +static int lfs2_init(lfs2_t *lfs2, const struct lfs2_config *cfg) { + lfs2->cfg = cfg; + int err = 0; + + // validate that the lfs2-cfg sizes were initiated properly before + // performing any arithmetic logics with them + LFS2_ASSERT(lfs2->cfg->read_size != 0); + LFS2_ASSERT(lfs2->cfg->prog_size != 0); + LFS2_ASSERT(lfs2->cfg->cache_size != 0); + + // check that block size is a multiple of cache size is a multiple + // of prog and read sizes + LFS2_ASSERT(lfs2->cfg->cache_size % lfs2->cfg->read_size == 0); + LFS2_ASSERT(lfs2->cfg->cache_size % lfs2->cfg->prog_size == 0); + LFS2_ASSERT(lfs2->cfg->block_size % lfs2->cfg->cache_size == 0); + + // check that the block size is large enough to fit ctz pointers + LFS2_ASSERT(4*lfs2_npw2(0xffffffff / (lfs2->cfg->block_size-2*4)) + <= lfs2->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. + LFS2_ASSERT(lfs2->cfg->block_cycles != 0); + + + // setup read cache + if (lfs2->cfg->read_buffer) { + lfs2->rcache.buffer = lfs2->cfg->read_buffer; + } else { + lfs2->rcache.buffer = lfs2_malloc(lfs2->cfg->cache_size); + if (!lfs2->rcache.buffer) { + err = LFS2_ERR_NOMEM; + goto cleanup; + } + } + + // setup program cache + if (lfs2->cfg->prog_buffer) { + lfs2->pcache.buffer = lfs2->cfg->prog_buffer; + } else { + lfs2->pcache.buffer = lfs2_malloc(lfs2->cfg->cache_size); + if (!lfs2->pcache.buffer) { + err = LFS2_ERR_NOMEM; + goto cleanup; + } + } + + // zero to avoid information leaks + lfs2_cache_zero(lfs2, &lfs2->rcache); + lfs2_cache_zero(lfs2, &lfs2->pcache); + + // setup lookahead, must be multiple of 64-bits, 32-bit aligned + LFS2_ASSERT(lfs2->cfg->lookahead_size > 0); + LFS2_ASSERT(lfs2->cfg->lookahead_size % 8 == 0 && + (uintptr_t)lfs2->cfg->lookahead_buffer % 4 == 0); + if (lfs2->cfg->lookahead_buffer) { + lfs2->free.buffer = lfs2->cfg->lookahead_buffer; + } else { + lfs2->free.buffer = lfs2_malloc(lfs2->cfg->lookahead_size); + if (!lfs2->free.buffer) { + err = LFS2_ERR_NOMEM; + goto cleanup; + } + } + + // check that the size limits are sane + LFS2_ASSERT(lfs2->cfg->name_max <= LFS2_NAME_MAX); + lfs2->name_max = lfs2->cfg->name_max; + if (!lfs2->name_max) { + lfs2->name_max = LFS2_NAME_MAX; + } + + LFS2_ASSERT(lfs2->cfg->file_max <= LFS2_FILE_MAX); + lfs2->file_max = lfs2->cfg->file_max; + if (!lfs2->file_max) { + lfs2->file_max = LFS2_FILE_MAX; + } + + LFS2_ASSERT(lfs2->cfg->attr_max <= LFS2_ATTR_MAX); + lfs2->attr_max = lfs2->cfg->attr_max; + if (!lfs2->attr_max) { + lfs2->attr_max = LFS2_ATTR_MAX; + } + + // setup default state + lfs2->root[0] = LFS2_BLOCK_NULL; + lfs2->root[1] = LFS2_BLOCK_NULL; + lfs2->mlist = NULL; + lfs2->seed = 0; + lfs2->gdisk = (lfs2_gstate_t){0}; + lfs2->gstate = (lfs2_gstate_t){0}; + lfs2->gdelta = (lfs2_gstate_t){0}; +#ifdef LFS2_MIGRATE + lfs2->lfs21 = NULL; +#endif + + return 0; + +cleanup: + lfs2_deinit(lfs2); + return err; +} + +static int lfs2_deinit(lfs2_t *lfs2) { + // free allocated memory + if (!lfs2->cfg->read_buffer) { + lfs2_free(lfs2->rcache.buffer); + } + + if (!lfs2->cfg->prog_buffer) { + lfs2_free(lfs2->pcache.buffer); + } + + if (!lfs2->cfg->lookahead_buffer) { + lfs2_free(lfs2->free.buffer); + } + + return 0; +} + +int lfs2_format(lfs2_t *lfs2, const struct lfs2_config *cfg) { + LFS2_TRACE("lfs2_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*)lfs2, (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 = lfs2_init(lfs2, cfg); + if (err) { + LFS2_TRACE("lfs2_format -> %d", err); + return err; + } + + // create free lookahead + memset(lfs2->free.buffer, 0, lfs2->cfg->lookahead_size); + lfs2->free.off = 0; + lfs2->free.size = lfs2_min(8*lfs2->cfg->lookahead_size, + lfs2->cfg->block_count); + lfs2->free.i = 0; + lfs2_alloc_ack(lfs2); + + // create root dir + lfs2_mdir_t root; + err = lfs2_dir_alloc(lfs2, &root); + if (err) { + goto cleanup; + } + + // write one superblock + lfs2_superblock_t superblock = { + .version = LFS2_DISK_VERSION, + .block_size = lfs2->cfg->block_size, + .block_count = lfs2->cfg->block_count, + .name_max = lfs2->name_max, + .file_max = lfs2->file_max, + .attr_max = lfs2->attr_max, + }; + + lfs2_superblock_tole32(&superblock); + err = lfs2_dir_commit(lfs2, &root, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_CREATE, 0, 0)}, + {LFS2_MKTAG(LFS2_TYPE_SUPERBLOCK, 0, 8), "littlefs"}, + {LFS2_MKTAG(LFS2_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock})); + if (err) { + goto cleanup; + } + + // sanity check that fetch works + err = lfs2_dir_fetch(lfs2, &root, (const lfs2_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 = lfs2_dir_commit(lfs2, &root, NULL, 0); + if (err) { + goto cleanup; + } + } + +cleanup: + lfs2_deinit(lfs2); + LFS2_TRACE("lfs2_format -> %d", err); + return err; +} + +int lfs2_mount(lfs2_t *lfs2, const struct lfs2_config *cfg) { + LFS2_TRACE("lfs2_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*)lfs2, (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 = lfs2_init(lfs2, cfg); + if (err) { + LFS2_TRACE("lfs2_mount -> %d", err); + return err; + } + + // scan directory blocks for superblock and any global updates + lfs2_mdir_t dir = {.tail = {0, 1}}; + lfs2_block_t cycle = 0; + while (!lfs2_pair_isnull(dir.tail)) { + if (cycle >= lfs2->cfg->block_count/2) { + // loop detected + err = LFS2_ERR_CORRUPT; + goto cleanup; + } + cycle += 1; + + // fetch next block in tail list + lfs2_stag_t tag = lfs2_dir_fetchmatch(lfs2, &dir, dir.tail, + LFS2_MKTAG(0x7ff, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_SUPERBLOCK, 0, 8), + NULL, + lfs2_dir_find_match, &(struct lfs2_dir_find_match){ + lfs2, "littlefs", 8}); + if (tag < 0) { + err = tag; + goto cleanup; + } + + // has superblock? + if (tag && !lfs2_tag_isdelete(tag)) { + // update root + lfs2->root[0] = dir.pair[0]; + lfs2->root[1] = dir.pair[1]; + + // grab superblock + lfs2_superblock_t superblock; + tag = lfs2_dir_get(lfs2, &dir, LFS2_MKTAG(0x7ff, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock); + if (tag < 0) { + err = tag; + goto cleanup; + } + lfs2_superblock_fromle32(&superblock); + + // check version + uint16_t major_version = (0xffff & (superblock.version >> 16)); + uint16_t minor_version = (0xffff & (superblock.version >> 0)); + if ((major_version != LFS2_DISK_VERSION_MAJOR || + minor_version > LFS2_DISK_VERSION_MINOR)) { + LFS2_ERROR("Invalid version v%"PRIu16".%"PRIu16, + major_version, minor_version); + err = LFS2_ERR_INVAL; + goto cleanup; + } + + // check superblock configuration + if (superblock.name_max) { + if (superblock.name_max > lfs2->name_max) { + LFS2_ERROR("Unsupported name_max (%"PRIu32" > %"PRIu32")", + superblock.name_max, lfs2->name_max); + err = LFS2_ERR_INVAL; + goto cleanup; + } + + lfs2->name_max = superblock.name_max; + } + + if (superblock.file_max) { + if (superblock.file_max > lfs2->file_max) { + LFS2_ERROR("Unsupported file_max (%"PRIu32" > %"PRIu32")", + superblock.file_max, lfs2->file_max); + err = LFS2_ERR_INVAL; + goto cleanup; + } + + lfs2->file_max = superblock.file_max; + } + + if (superblock.attr_max) { + if (superblock.attr_max > lfs2->attr_max) { + LFS2_ERROR("Unsupported attr_max (%"PRIu32" > %"PRIu32")", + superblock.attr_max, lfs2->attr_max); + err = LFS2_ERR_INVAL; + goto cleanup; + } + + lfs2->attr_max = superblock.attr_max; + } + } + + // has gstate? + err = lfs2_dir_getgstate(lfs2, &dir, &lfs2->gstate); + if (err) { + goto cleanup; + } + } + + // found superblock? + if (lfs2_pair_isnull(lfs2->root)) { + err = LFS2_ERR_INVAL; + goto cleanup; + } + + // update littlefs with gstate + if (!lfs2_gstate_iszero(&lfs2->gstate)) { + LFS2_DEBUG("Found pending gstate 0x%08"PRIx32"%08"PRIx32"%08"PRIx32, + lfs2->gstate.tag, + lfs2->gstate.pair[0], + lfs2->gstate.pair[1]); + } + lfs2->gstate.tag += !lfs2_tag_isvalid(lfs2->gstate.tag); + lfs2->gdisk = lfs2->gstate; + + // setup free lookahead + lfs2_alloc_reset(lfs2); + + LFS2_TRACE("lfs2_mount -> %d", 0); + return 0; + +cleanup: + lfs2_unmount(lfs2); + LFS2_TRACE("lfs2_mount -> %d", err); + return err; +} + +int lfs2_unmount(lfs2_t *lfs2) { + LFS2_TRACE("lfs2_unmount(%p)", (void*)lfs2); + int err = lfs2_deinit(lfs2); + LFS2_TRACE("lfs2_unmount -> %d", err); + return err; +} + + +/// Filesystem filesystem operations /// +int lfs2_fs_traverseraw(lfs2_t *lfs2, + int (*cb)(void *data, lfs2_block_t block), void *data, + bool includeorphans) { + // iterate over metadata pairs + lfs2_mdir_t dir = {.tail = {0, 1}}; + +#ifdef LFS2_MIGRATE + // also consider v1 blocks during migration + if (lfs2->lfs21) { + int err = lfs21_traverse(lfs2, cb, data); + if (err) { + return err; + } + + dir.tail[0] = lfs2->root[0]; + dir.tail[1] = lfs2->root[1]; + } +#endif + + lfs2_block_t cycle = 0; + while (!lfs2_pair_isnull(dir.tail)) { + if (cycle >= lfs2->cfg->block_count/2) { + // loop detected + return LFS2_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 = lfs2_dir_fetch(lfs2, &dir, dir.tail); + if (err) { + return err; + } + + for (uint16_t id = 0; id < dir.count; id++) { + struct lfs2_ctz ctz; + lfs2_stag_t tag = lfs2_dir_get(lfs2, &dir, LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, id, sizeof(ctz)), &ctz); + if (tag < 0) { + if (tag == LFS2_ERR_NOENT) { + continue; + } + return tag; + } + lfs2_ctz_fromle32(&ctz); + + if (lfs2_tag_type3(tag) == LFS2_TYPE_CTZSTRUCT) { + err = lfs2_ctz_traverse(lfs2, NULL, &lfs2->rcache, + ctz.head, ctz.size, cb, data); + if (err) { + return err; + } + } else if (includeorphans && + lfs2_tag_type3(tag) == LFS2_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 (lfs2_file_t *f = (lfs2_file_t*)lfs2->mlist; f; f = f->next) { + if (f->type != LFS2_TYPE_REG) { + continue; + } + + if ((f->flags & LFS2_F_DIRTY) && !(f->flags & LFS2_F_INLINE)) { + int err = lfs2_ctz_traverse(lfs2, &f->cache, &lfs2->rcache, + f->ctz.head, f->ctz.size, cb, data); + if (err) { + return err; + } + } + + if ((f->flags & LFS2_F_WRITING) && !(f->flags & LFS2_F_INLINE)) { + int err = lfs2_ctz_traverse(lfs2, &f->cache, &lfs2->rcache, + f->block, f->pos, cb, data); + if (err) { + return err; + } + } + } + + return 0; +} + +int lfs2_fs_traverse(lfs2_t *lfs2, + int (*cb)(void *data, lfs2_block_t block), void *data) { + LFS2_TRACE("lfs2_fs_traverse(%p, %p, %p)", + (void*)lfs2, (void*)(uintptr_t)cb, data); + int err = lfs2_fs_traverseraw(lfs2, cb, data, true); + LFS2_TRACE("lfs2_fs_traverse -> %d", 0); + return err; +} + +static int lfs2_fs_pred(lfs2_t *lfs2, + const lfs2_block_t pair[2], lfs2_mdir_t *pdir) { + // iterate over all directory directory entries + pdir->tail[0] = 0; + pdir->tail[1] = 1; + lfs2_block_t cycle = 0; + while (!lfs2_pair_isnull(pdir->tail)) { + if (cycle >= lfs2->cfg->block_count/2) { + // loop detected + return LFS2_ERR_CORRUPT; + } + cycle += 1; + + if (lfs2_pair_cmp(pdir->tail, pair) == 0) { + return 0; + } + + int err = lfs2_dir_fetch(lfs2, pdir, pdir->tail); + if (err) { + return err; + } + } + + return LFS2_ERR_NOENT; +} + +struct lfs2_fs_parent_match { + lfs2_t *lfs2; + const lfs2_block_t pair[2]; +}; + +static int lfs2_fs_parent_match(void *data, + lfs2_tag_t tag, const void *buffer) { + struct lfs2_fs_parent_match *find = data; + lfs2_t *lfs2 = find->lfs2; + const struct lfs2_diskoff *disk = buffer; + (void)tag; + + lfs2_block_t child[2]; + int err = lfs2_bd_read(lfs2, + &lfs2->pcache, &lfs2->rcache, lfs2->cfg->block_size, + disk->block, disk->off, &child, sizeof(child)); + if (err) { + return err; + } + + lfs2_pair_fromle32(child); + return (lfs2_pair_cmp(child, find->pair) == 0) ? LFS2_CMP_EQ : LFS2_CMP_LT; +} + +static lfs2_stag_t lfs2_fs_parent(lfs2_t *lfs2, const lfs2_block_t pair[2], + lfs2_mdir_t *parent) { + // use fetchmatch with callback to find pairs + parent->tail[0] = 0; + parent->tail[1] = 1; + lfs2_block_t cycle = 0; + while (!lfs2_pair_isnull(parent->tail)) { + if (cycle >= lfs2->cfg->block_count/2) { + // loop detected + return LFS2_ERR_CORRUPT; + } + cycle += 1; + + lfs2_stag_t tag = lfs2_dir_fetchmatch(lfs2, parent, parent->tail, + LFS2_MKTAG(0x7ff, 0, 0x3ff), + LFS2_MKTAG(LFS2_TYPE_DIRSTRUCT, 0, 8), + NULL, + lfs2_fs_parent_match, &(struct lfs2_fs_parent_match){ + lfs2, {pair[0], pair[1]}}); + if (tag && tag != LFS2_ERR_NOENT) { + return tag; + } + } + + return LFS2_ERR_NOENT; +} + +static int lfs2_fs_relocate(lfs2_t *lfs2, + const lfs2_block_t oldpair[2], lfs2_block_t newpair[2]) { + // update internal root + if (lfs2_pair_cmp(oldpair, lfs2->root) == 0) { + lfs2->root[0] = newpair[0]; + lfs2->root[1] = newpair[1]; + } + + // update internally tracked dirs + for (struct lfs2_mlist *d = lfs2->mlist; d; d = d->next) { + if (lfs2_pair_cmp(oldpair, d->m.pair) == 0) { + d->m.pair[0] = newpair[0]; + d->m.pair[1] = newpair[1]; + } + + if (d->type == LFS2_TYPE_DIR && + lfs2_pair_cmp(oldpair, ((lfs2_dir_t*)d)->head) == 0) { + ((lfs2_dir_t*)d)->head[0] = newpair[0]; + ((lfs2_dir_t*)d)->head[1] = newpair[1]; + } + } + + // find parent + lfs2_mdir_t parent; + lfs2_stag_t tag = lfs2_fs_parent(lfs2, oldpair, &parent); + if (tag < 0 && tag != LFS2_ERR_NOENT) { + return tag; + } + + if (tag != LFS2_ERR_NOENT) { + // update disk, this creates a desync + lfs2_fs_preporphans(lfs2, +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 (lfs2_gstate_hasmovehere(&lfs2->gstate, parent.pair)) { + moveid = lfs2_tag_id(lfs2->gstate.tag); + LFS2_DEBUG("Fixing move while relocating " + "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n", + parent.pair[0], parent.pair[1], moveid); + lfs2_fs_prepmove(lfs2, 0x3ff, NULL); + if (moveid < lfs2_tag_id(tag)) { + tag -= LFS2_MKTAG(0, 1, 0); + } + } + + lfs2_pair_tole32(newpair); + int err = lfs2_dir_commit(lfs2, &parent, LFS2_MKATTRS( + {LFS2_MKTAG_IF(moveid != 0x3ff, + LFS2_TYPE_DELETE, moveid, 0)}, + {tag, newpair})); + lfs2_pair_fromle32(newpair); + if (err) { + return err; + } + + // next step, clean up orphans + lfs2_fs_preporphans(lfs2, -1); + } + + // find pred + int err = lfs2_fs_pred(lfs2, oldpair, &parent); + if (err && err != LFS2_ERR_NOENT) { + return err; + } + + // if we can't find dir, it must be new + if (err != LFS2_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 (lfs2_gstate_hasmovehere(&lfs2->gstate, parent.pair)) { + moveid = lfs2_tag_id(lfs2->gstate.tag); + LFS2_DEBUG("Fixing move while relocating " + "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n", + parent.pair[0], parent.pair[1], moveid); + lfs2_fs_prepmove(lfs2, 0x3ff, NULL); + } + + // replace bad pair, either we clean up desync, or no desync occured + lfs2_pair_tole32(newpair); + err = lfs2_dir_commit(lfs2, &parent, LFS2_MKATTRS( + {LFS2_MKTAG_IF(moveid != 0x3ff, + LFS2_TYPE_DELETE, moveid, 0)}, + {LFS2_MKTAG(LFS2_TYPE_TAIL + parent.split, 0x3ff, 8), newpair})); + lfs2_pair_fromle32(newpair); + if (err) { + return err; + } + } + + return 0; +} + +static void lfs2_fs_preporphans(lfs2_t *lfs2, int8_t orphans) { + LFS2_ASSERT(lfs2_tag_size(lfs2->gstate.tag) > 0 || orphans >= 0); + lfs2->gstate.tag += orphans; + lfs2->gstate.tag = ((lfs2->gstate.tag & ~LFS2_MKTAG(0x800, 0, 0)) | + ((uint32_t)lfs2_gstate_hasorphans(&lfs2->gstate) << 31)); +} + +static void lfs2_fs_prepmove(lfs2_t *lfs2, + uint16_t id, const lfs2_block_t pair[2]) { + lfs2->gstate.tag = ((lfs2->gstate.tag & ~LFS2_MKTAG(0x7ff, 0x3ff, 0)) | + ((id != 0x3ff) ? LFS2_MKTAG(LFS2_TYPE_DELETE, id, 0) : 0)); + lfs2->gstate.pair[0] = (id != 0x3ff) ? pair[0] : 0; + lfs2->gstate.pair[1] = (id != 0x3ff) ? pair[1] : 0; +} + +static int lfs2_fs_demove(lfs2_t *lfs2) { + if (!lfs2_gstate_hasmove(&lfs2->gdisk)) { + return 0; + } + + // Fix bad moves + LFS2_DEBUG("Fixing move {0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16, + lfs2->gdisk.pair[0], + lfs2->gdisk.pair[1], + lfs2_tag_id(lfs2->gdisk.tag)); + + // fetch and delete the moved entry + lfs2_mdir_t movedir; + int err = lfs2_dir_fetch(lfs2, &movedir, lfs2->gdisk.pair); + if (err) { + return err; + } + + // prep gstate and delete move id + uint16_t moveid = lfs2_tag_id(lfs2->gdisk.tag); + lfs2_fs_prepmove(lfs2, 0x3ff, NULL); + err = lfs2_dir_commit(lfs2, &movedir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_DELETE, moveid, 0)})); + if (err) { + return err; + } + + return 0; +} + +static int lfs2_fs_deorphan(lfs2_t *lfs2) { + if (!lfs2_gstate_hasorphans(&lfs2->gstate)) { + return 0; + } + + // Fix any orphans + lfs2_mdir_t pdir = {.split = true, .tail = {0, 1}}; + lfs2_mdir_t dir; + + // iterate over all directory directory entries + while (!lfs2_pair_isnull(pdir.tail)) { + int err = lfs2_dir_fetch(lfs2, &dir, pdir.tail); + if (err) { + return err; + } + + // check head blocks for orphans + if (!pdir.split) { + // check if we have a parent + lfs2_mdir_t parent; + lfs2_stag_t tag = lfs2_fs_parent(lfs2, pdir.tail, &parent); + if (tag < 0 && tag != LFS2_ERR_NOENT) { + return tag; + } + + if (tag == LFS2_ERR_NOENT) { + // we are an orphan + LFS2_DEBUG("Fixing orphan {0x%"PRIx32", 0x%"PRIx32"}", + pdir.tail[0], pdir.tail[1]); + + err = lfs2_dir_drop(lfs2, &pdir, &dir); + if (err) { + return err; + } + + // refetch tail + continue; + } + + lfs2_block_t pair[2]; + lfs2_stag_t res = lfs2_dir_get(lfs2, &parent, + LFS2_MKTAG(0x7ff, 0x3ff, 0), tag, pair); + if (res < 0) { + return res; + } + lfs2_pair_fromle32(pair); + + if (!lfs2_pair_sync(pair, pdir.tail)) { + // we have desynced + LFS2_DEBUG("Fixing half-orphan {0x%"PRIx32", 0x%"PRIx32"} " + "-> {0x%"PRIx32", 0x%"PRIx32"}", + pdir.tail[0], pdir.tail[1], pair[0], pair[1]); + + lfs2_pair_tole32(pair); + err = lfs2_dir_commit(lfs2, &pdir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_SOFTTAIL, 0x3ff, 8), pair})); + lfs2_pair_fromle32(pair); + if (err) { + return err; + } + + // refetch tail + continue; + } + } + + pdir = dir; + } + + // mark orphans as fixed + lfs2_fs_preporphans(lfs2, -lfs2_gstate_getorphans(&lfs2->gstate)); + return 0; +} + +static int lfs2_fs_forceconsistency(lfs2_t *lfs2) { + int err = lfs2_fs_demove(lfs2); + if (err) { + return err; + } + + err = lfs2_fs_deorphan(lfs2); + if (err) { + return err; + } + + return 0; +} + +static int lfs2_fs_size_count(void *p, lfs2_block_t block) { + (void)block; + lfs2_size_t *size = p; + *size += 1; + return 0; +} + +lfs2_ssize_t lfs2_fs_size(lfs2_t *lfs2) { + LFS2_TRACE("lfs2_fs_size(%p)", (void*)lfs2); + lfs2_size_t size = 0; + int err = lfs2_fs_traverseraw(lfs2, lfs2_fs_size_count, &size, false); + if (err) { + LFS2_TRACE("lfs2_fs_size -> %d", err); + return err; + } + + LFS2_TRACE("lfs2_fs_size -> %d", err); + return size; +} + +#ifdef LFS2_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 LFS21_VERSION 0x00010007 +#define LFS21_VERSION_MAJOR (0xffff & (LFS21_VERSION >> 16)) +#define LFS21_VERSION_MINOR (0xffff & (LFS21_VERSION >> 0)) + +// Version of On-disk data structures +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS21_DISK_VERSION 0x00010001 +#define LFS21_DISK_VERSION_MAJOR (0xffff & (LFS21_DISK_VERSION >> 16)) +#define LFS21_DISK_VERSION_MINOR (0xffff & (LFS21_DISK_VERSION >> 0)) + + +/// v1 Definitions /// + +// File types +enum lfs21_type { + LFS21_TYPE_REG = 0x11, + LFS21_TYPE_DIR = 0x22, + LFS21_TYPE_SUPERBLOCK = 0x2e, +}; + +typedef struct lfs21 { + lfs2_block_t root[2]; +} lfs21_t; + +typedef struct lfs21_entry { + lfs2_off_t off; + + struct lfs21_disk_entry { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + union { + struct { + lfs2_block_t head; + lfs2_size_t size; + } file; + lfs2_block_t dir[2]; + } u; + } d; +} lfs21_entry_t; + +typedef struct lfs21_dir { + struct lfs21_dir *next; + lfs2_block_t pair[2]; + lfs2_off_t off; + + lfs2_block_t head[2]; + lfs2_off_t pos; + + struct lfs21_disk_dir { + uint32_t rev; + lfs2_size_t size; + lfs2_block_t tail[2]; + } d; +} lfs21_dir_t; + +typedef struct lfs21_superblock { + lfs2_off_t off; + + struct lfs21_disk_superblock { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + lfs2_block_t root[2]; + uint32_t block_size; + uint32_t block_count; + uint32_t version; + char magic[8]; + } d; +} lfs21_superblock_t; + + +/// Low-level wrappers v1->v2 /// +static void lfs21_crc(uint32_t *crc, const void *buffer, size_t size) { + *crc = lfs2_crc(*crc, buffer, size); +} + +static int lfs21_bd_read(lfs2_t *lfs2, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_size_t size) { + // if we ever do more than writes to alternating pairs, + // this may need to consider pcache + return lfs2_bd_read(lfs2, &lfs2->pcache, &lfs2->rcache, size, + block, off, buffer, size); +} + +static int lfs21_bd_crc(lfs2_t *lfs2, lfs2_block_t block, + lfs2_off_t off, lfs2_size_t size, uint32_t *crc) { + for (lfs2_off_t i = 0; i < size; i++) { + uint8_t c; + int err = lfs21_bd_read(lfs2, block, off+i, &c, 1); + if (err) { + return err; + } + + lfs21_crc(crc, &c, 1); + } + + return 0; +} + + +/// Endian swapping functions /// +static void lfs21_dir_fromle32(struct lfs21_disk_dir *d) { + d->rev = lfs2_fromle32(d->rev); + d->size = lfs2_fromle32(d->size); + d->tail[0] = lfs2_fromle32(d->tail[0]); + d->tail[1] = lfs2_fromle32(d->tail[1]); +} + +static void lfs21_dir_tole32(struct lfs21_disk_dir *d) { + d->rev = lfs2_tole32(d->rev); + d->size = lfs2_tole32(d->size); + d->tail[0] = lfs2_tole32(d->tail[0]); + d->tail[1] = lfs2_tole32(d->tail[1]); +} + +static void lfs21_entry_fromle32(struct lfs21_disk_entry *d) { + d->u.dir[0] = lfs2_fromle32(d->u.dir[0]); + d->u.dir[1] = lfs2_fromle32(d->u.dir[1]); +} + +static void lfs21_entry_tole32(struct lfs21_disk_entry *d) { + d->u.dir[0] = lfs2_tole32(d->u.dir[0]); + d->u.dir[1] = lfs2_tole32(d->u.dir[1]); +} + +static void lfs21_superblock_fromle32(struct lfs21_disk_superblock *d) { + d->root[0] = lfs2_fromle32(d->root[0]); + d->root[1] = lfs2_fromle32(d->root[1]); + d->block_size = lfs2_fromle32(d->block_size); + d->block_count = lfs2_fromle32(d->block_count); + d->version = lfs2_fromle32(d->version); +} + + +///// Metadata pair and directory operations /// +static inline lfs2_size_t lfs21_entry_size(const lfs21_entry_t *entry) { + return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; +} + +static int lfs21_dir_fetch(lfs2_t *lfs2, + lfs21_dir_t *dir, const lfs2_block_t pair[2]) { + // copy out pair, otherwise may be aliasing dir + const lfs2_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 lfs21_disk_dir test; + int err = lfs21_bd_read(lfs2, tpair[i], 0, &test, sizeof(test)); + lfs21_dir_fromle32(&test); + if (err) { + if (err == LFS2_ERR_CORRUPT) { + continue; + } + return err; + } + + if (valid && lfs2_scmp(test.rev, dir->d.rev) < 0) { + continue; + } + + if ((0x7fffffff & test.size) < sizeof(test)+4 || + (0x7fffffff & test.size) > lfs2->cfg->block_size) { + continue; + } + + uint32_t crc = 0xffffffff; + lfs21_dir_tole32(&test); + lfs21_crc(&crc, &test, sizeof(test)); + lfs21_dir_fromle32(&test); + err = lfs21_bd_crc(lfs2, tpair[i], sizeof(test), + (0x7fffffff & test.size) - sizeof(test), &crc); + if (err) { + if (err == LFS2_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) { + LFS2_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}", + tpair[0], tpair[1]); + return LFS2_ERR_CORRUPT; + } + + return 0; +} + +static int lfs21_dir_next(lfs2_t *lfs2, lfs21_dir_t *dir, lfs21_entry_t *entry) { + while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)-4) { + if (!(0x80000000 & dir->d.size)) { + entry->off = dir->off; + return LFS2_ERR_NOENT; + } + + int err = lfs21_dir_fetch(lfs2, dir, dir->d.tail); + if (err) { + return err; + } + + dir->off = sizeof(dir->d); + dir->pos += sizeof(dir->d) + 4; + } + + int err = lfs21_bd_read(lfs2, dir->pair[0], dir->off, + &entry->d, sizeof(entry->d)); + lfs21_entry_fromle32(&entry->d); + if (err) { + return err; + } + + entry->off = dir->off; + dir->off += lfs21_entry_size(entry); + dir->pos += lfs21_entry_size(entry); + return 0; +} + +/// littlefs v1 specific operations /// +int lfs21_traverse(lfs2_t *lfs2, int (*cb)(void*, lfs2_block_t), void *data) { + if (lfs2_pair_isnull(lfs2->lfs21->root)) { + return 0; + } + + // iterate over metadata pairs + lfs21_dir_t dir; + lfs21_entry_t entry; + lfs2_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 = lfs21_dir_fetch(lfs2, &dir, cwd); + if (err) { + return err; + } + + // iterate over contents + while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size)-4) { + err = lfs21_bd_read(lfs2, dir.pair[0], dir.off, + &entry.d, sizeof(entry.d)); + lfs21_entry_fromle32(&entry.d); + if (err) { + return err; + } + + dir.off += lfs21_entry_size(&entry); + if ((0x70 & entry.d.type) == (0x70 & LFS21_TYPE_REG)) { + err = lfs2_ctz_traverse(lfs2, NULL, &lfs2->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 + lfs2_mdir_t dir2 = {.split=true, .tail={cwd[0], cwd[1]}}; + while (dir2.split) { + err = lfs2_dir_fetch(lfs2, &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 (lfs2_pair_isnull(cwd)) { + break; + } + } + + return 0; +} + +static int lfs21_moved(lfs2_t *lfs2, const void *e) { + if (lfs2_pair_isnull(lfs2->lfs21->root)) { + return 0; + } + + // skip superblock + lfs21_dir_t cwd; + int err = lfs21_dir_fetch(lfs2, &cwd, (const lfs2_block_t[2]){0, 1}); + if (err) { + return err; + } + + // iterate over all directory directory entries + lfs21_entry_t entry; + while (!lfs2_pair_isnull(cwd.d.tail)) { + err = lfs21_dir_fetch(lfs2, &cwd, cwd.d.tail); + if (err) { + return err; + } + + while (true) { + err = lfs21_dir_next(lfs2, &cwd, &entry); + if (err && err != LFS2_ERR_NOENT) { + return err; + } + + if (err == LFS2_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 lfs21_mount(lfs2_t *lfs2, struct lfs21 *lfs21, + const struct lfs2_config *cfg) { + int err = 0; + { + err = lfs2_init(lfs2, cfg); + if (err) { + return err; + } + + lfs2->lfs21 = lfs21; + lfs2->lfs21->root[0] = LFS2_BLOCK_NULL; + lfs2->lfs21->root[1] = LFS2_BLOCK_NULL; + + // setup free lookahead + lfs2->free.off = 0; + lfs2->free.size = 0; + lfs2->free.i = 0; + lfs2_alloc_ack(lfs2); + + // load superblock + lfs21_dir_t dir; + lfs21_superblock_t superblock; + err = lfs21_dir_fetch(lfs2, &dir, (const lfs2_block_t[2]){0, 1}); + if (err && err != LFS2_ERR_CORRUPT) { + goto cleanup; + } + + if (!err) { + err = lfs21_bd_read(lfs2, dir.pair[0], sizeof(dir.d), + &superblock.d, sizeof(superblock.d)); + lfs21_superblock_fromle32(&superblock.d); + if (err) { + goto cleanup; + } + + lfs2->lfs21->root[0] = superblock.d.root[0]; + lfs2->lfs21->root[1] = superblock.d.root[1]; + } + + if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { + LFS2_ERROR("Invalid superblock at {0x%"PRIx32", 0x%"PRIx32"}", + 0, 1); + err = LFS2_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 != LFS21_DISK_VERSION_MAJOR || + minor_version > LFS21_DISK_VERSION_MINOR)) { + LFS2_ERROR("Invalid version v%d.%d", major_version, minor_version); + err = LFS2_ERR_INVAL; + goto cleanup; + } + + return 0; + } + +cleanup: + lfs2_deinit(lfs2); + return err; +} + +static int lfs21_unmount(lfs2_t *lfs2) { + return lfs2_deinit(lfs2); +} + +/// v1 migration /// +int lfs2_migrate(lfs2_t *lfs2, const struct lfs2_config *cfg) { + LFS2_TRACE("lfs2_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*)lfs2, (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 lfs21 lfs21; + int err = lfs21_mount(lfs2, &lfs21, cfg); + if (err) { + LFS2_TRACE("lfs2_migrate -> %d", err); + return err; + } + + { + // iterate through each directory, copying over entries + // into new directory + lfs21_dir_t dir1; + lfs2_mdir_t dir2; + dir1.d.tail[0] = lfs2->lfs21->root[0]; + dir1.d.tail[1] = lfs2->lfs21->root[1]; + while (!lfs2_pair_isnull(dir1.d.tail)) { + // iterate old dir + err = lfs21_dir_fetch(lfs2, &dir1, dir1.d.tail); + if (err) { + goto cleanup; + } + + // create new dir and bind as temporary pretend root + err = lfs2_dir_alloc(lfs2, &dir2); + if (err) { + goto cleanup; + } + + dir2.rev = dir1.d.rev; + dir1.head[0] = dir1.pair[0]; + dir1.head[1] = dir1.pair[1]; + lfs2->root[0] = dir2.pair[0]; + lfs2->root[1] = dir2.pair[1]; + + err = lfs2_dir_commit(lfs2, &dir2, NULL, 0); + if (err) { + goto cleanup; + } + + while (true) { + lfs21_entry_t entry1; + err = lfs21_dir_next(lfs2, &dir1, &entry1); + if (err && err != LFS2_ERR_NOENT) { + goto cleanup; + } + + if (err == LFS2_ERR_NOENT) { + break; + } + + // check that entry has not been moved + if (entry1.d.type & 0x80) { + int moved = lfs21_moved(lfs2, &entry1.d.u); + if (moved < 0) { + err = moved; + goto cleanup; + } + + if (moved) { + continue; + } + + entry1.d.type &= ~0x80; + } + + // also fetch name + char name[LFS2_NAME_MAX+1]; + memset(name, 0, sizeof(name)); + err = lfs21_bd_read(lfs2, 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 == LFS21_TYPE_DIR); + + // create entry in new dir + err = lfs2_dir_fetch(lfs2, &dir2, lfs2->root); + if (err) { + goto cleanup; + } + + uint16_t id; + err = lfs2_dir_find(lfs2, &dir2, &(const char*){name}, &id); + if (!(err == LFS2_ERR_NOENT && id != 0x3ff)) { + err = (err < 0) ? err : LFS2_ERR_EXIST; + goto cleanup; + } + + lfs21_entry_tole32(&entry1.d); + err = lfs2_dir_commit(lfs2, &dir2, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_CREATE, id, 0)}, + {LFS2_MKTAG_IF_ELSE(isdir, + LFS2_TYPE_DIR, id, entry1.d.nlen, + LFS2_TYPE_REG, id, entry1.d.nlen), + name}, + {LFS2_MKTAG_IF_ELSE(isdir, + LFS2_TYPE_DIRSTRUCT, id, sizeof(entry1.d.u), + LFS2_TYPE_CTZSTRUCT, id, sizeof(entry1.d.u)), + &entry1.d.u})); + lfs21_entry_fromle32(&entry1.d); + if (err) { + goto cleanup; + } + } + + if (!lfs2_pair_isnull(dir1.d.tail)) { + // find last block and update tail to thread into fs + err = lfs2_dir_fetch(lfs2, &dir2, lfs2->root); + if (err) { + goto cleanup; + } + + while (dir2.split) { + err = lfs2_dir_fetch(lfs2, &dir2, dir2.tail); + if (err) { + goto cleanup; + } + } + + lfs2_pair_tole32(dir2.pair); + err = lfs2_dir_commit(lfs2, &dir2, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_SOFTTAIL, 0x3ff, 8), dir1.d.tail})); + lfs2_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. + LFS2_DEBUG("Migrating {0x%"PRIx32", 0x%"PRIx32"} " + "-> {0x%"PRIx32", 0x%"PRIx32"}", + lfs2->root[0], lfs2->root[1], dir1.head[0], dir1.head[1]); + + err = lfs2_bd_erase(lfs2, dir1.head[1]); + if (err) { + goto cleanup; + } + + err = lfs2_dir_fetch(lfs2, &dir2, lfs2->root); + if (err) { + goto cleanup; + } + + for (lfs2_off_t i = 0; i < dir2.off; i++) { + uint8_t dat; + err = lfs2_bd_read(lfs2, + NULL, &lfs2->rcache, dir2.off, + dir2.pair[0], i, &dat, 1); + if (err) { + goto cleanup; + } + + err = lfs2_bd_prog(lfs2, + &lfs2->pcache, &lfs2->rcache, true, + dir1.head[1], i, &dat, 1); + if (err) { + goto cleanup; + } + } + + err = lfs2_bd_flush(lfs2, &lfs2->pcache, &lfs2->rcache, true); + if (err) { + goto cleanup; + } + } + + // Create new superblock. This marks a successful migration! + err = lfs21_dir_fetch(lfs2, &dir1, (const lfs2_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] = lfs2->lfs21->root[0]; + dir2.tail[1] = lfs2->lfs21->root[1]; + dir2.erased = false; + dir2.split = true; + + lfs2_superblock_t superblock = { + .version = LFS2_DISK_VERSION, + .block_size = lfs2->cfg->block_size, + .block_count = lfs2->cfg->block_count, + .name_max = lfs2->name_max, + .file_max = lfs2->file_max, + .attr_max = lfs2->attr_max, + }; + + lfs2_superblock_tole32(&superblock); + err = lfs2_dir_commit(lfs2, &dir2, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_CREATE, 0, 0)}, + {LFS2_MKTAG(LFS2_TYPE_SUPERBLOCK, 0, 8), "littlefs"}, + {LFS2_MKTAG(LFS2_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock})); + if (err) { + goto cleanup; + } + + // sanity check that fetch works + err = lfs2_dir_fetch(lfs2, &dir2, (const lfs2_block_t[2]){0, 1}); + if (err) { + goto cleanup; + } + + // force compaction to prevent accidentally mounting v1 + dir2.erased = false; + err = lfs2_dir_commit(lfs2, &dir2, NULL, 0); + if (err) { + goto cleanup; + } + } + +cleanup: + lfs21_unmount(lfs2); + LFS2_TRACE("lfs2_migrate -> %d", err); + return err; +} + +#endif diff --git a/features/storage/filesystem/littlefsv2/littlefs/lfs2.h b/features/storage/filesystem/littlefsv2/littlefs/lfs2.h new file mode 100644 index 00000000000..c89af79cabd --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/lfs2.h @@ -0,0 +1,655 @@ +/* + * The little filesystem + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS2_H +#define LFS2_H + +#include +#include + +#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 LFS2_VERSION 0x00020002 +#define LFS2_VERSION_MAJOR (0xffff & (LFS2_VERSION >> 16)) +#define LFS2_VERSION_MINOR (0xffff & (LFS2_VERSION >> 0)) + +// Version of On-disk data structures +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS2_DISK_VERSION 0x00020000 +#define LFS2_DISK_VERSION_MAJOR (0xffff & (LFS2_DISK_VERSION >> 16)) +#define LFS2_DISK_VERSION_MINOR (0xffff & (LFS2_DISK_VERSION >> 0)) + + +/// Definitions /// + +// Type definitions +typedef uint32_t lfs2_size_t; +typedef uint32_t lfs2_off_t; + +typedef int32_t lfs2_ssize_t; +typedef int32_t lfs2_soff_t; + +typedef uint32_t lfs2_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 LFS2_NAME_MAX +#define LFS2_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 lfs2_file_seek, lfs2_file_size, and lfs2_file_tell will return +// incorrect values due to using signed integers. Stored in superblock and +// must be respected by other littlefs drivers. +#ifndef LFS2_FILE_MAX +#define LFS2_FILE_MAX 2147483647 +#endif + +// Maximum size of custom attributes in bytes, may be redefined, but there is +// no real benefit to using a smaller LFS2_ATTR_MAX. Limited to <= 1022. +#ifndef LFS2_ATTR_MAX +#define LFS2_ATTR_MAX 1022 +#endif + +// Possible error codes, these are negative to allow +// valid positive return values +enum lfs2_error { + LFS2_ERR_OK = 0, // No error + LFS2_ERR_IO = -5, // Error during device operation + LFS2_ERR_CORRUPT = -84, // Corrupted + LFS2_ERR_NOENT = -2, // No directory entry + LFS2_ERR_EXIST = -17, // Entry already exists + LFS2_ERR_NOTDIR = -20, // Entry is not a dir + LFS2_ERR_ISDIR = -21, // Entry is a dir + LFS2_ERR_NOTEMPTY = -39, // Dir is not empty + LFS2_ERR_BADF = -9, // Bad file number + LFS2_ERR_FBIG = -27, // File too large + LFS2_ERR_INVAL = -22, // Invalid parameter + LFS2_ERR_NOSPC = -28, // No space left on device + LFS2_ERR_NOMEM = -12, // No more memory available + LFS2_ERR_NOATTR = -61, // No data/attr available + LFS2_ERR_NAMETOOLONG = -36, // File name too long +}; + +// File types +enum lfs2_type { + // file types + LFS2_TYPE_REG = 0x001, + LFS2_TYPE_DIR = 0x002, + + // internally used types + LFS2_TYPE_SPLICE = 0x400, + LFS2_TYPE_NAME = 0x000, + LFS2_TYPE_STRUCT = 0x200, + LFS2_TYPE_USERATTR = 0x300, + LFS2_TYPE_FROM = 0x100, + LFS2_TYPE_TAIL = 0x600, + LFS2_TYPE_GLOBALS = 0x700, + LFS2_TYPE_CRC = 0x500, + + // internally used type specializations + LFS2_TYPE_CREATE = 0x401, + LFS2_TYPE_DELETE = 0x4ff, + LFS2_TYPE_SUPERBLOCK = 0x0ff, + LFS2_TYPE_DIRSTRUCT = 0x200, + LFS2_TYPE_CTZSTRUCT = 0x202, + LFS2_TYPE_INLINESTRUCT = 0x201, + LFS2_TYPE_SOFTTAIL = 0x600, + LFS2_TYPE_HARDTAIL = 0x601, + LFS2_TYPE_MOVESTATE = 0x7ff, + + // internal chip sources + LFS2_FROM_NOOP = 0x000, + LFS2_FROM_MOVE = 0x101, + LFS2_FROM_USERATTRS = 0x102, +}; + +// File open flags +enum lfs2_open_flags { + // open flags + LFS2_O_RDONLY = 1, // Open a file as read only + LFS2_O_WRONLY = 2, // Open a file as write only + LFS2_O_RDWR = 3, // Open a file as read and write + LFS2_O_CREAT = 0x0100, // Create a file if it does not exist + LFS2_O_EXCL = 0x0200, // Fail if a file already exists + LFS2_O_TRUNC = 0x0400, // Truncate the existing file to zero size + LFS2_O_APPEND = 0x0800, // Move to end of file on every write + + // internally used flags + LFS2_F_DIRTY = 0x010000, // File does not match storage + LFS2_F_WRITING = 0x020000, // File has been written since last flush + LFS2_F_READING = 0x040000, // File has been read since last flush + LFS2_F_ERRED = 0x080000, // An error occured during write + LFS2_F_INLINE = 0x100000, // Currently inlined in directory entry + LFS2_F_OPENED = 0x200000, // File has been opened +}; + +// File seek flags +enum lfs2_whence_flags { + LFS2_SEEK_SET = 0, // Seek relative to an absolute position + LFS2_SEEK_CUR = 1, // Seek relative to the current file position + LFS2_SEEK_END = 2, // Seek relative to the end of the file +}; + + +// Configuration provided during initialization of the littlefs +struct lfs2_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 lfs2_config *c, lfs2_block_t block, + lfs2_off_t off, void *buffer, lfs2_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 LFS2_ERR_CORRUPT if the block should be considered bad. + int (*prog)(const struct lfs2_config *c, lfs2_block_t block, + lfs2_off_t off, const void *buffer, lfs2_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 LFS2_ERR_CORRUPT if the block should be considered bad. + int (*erase)(const struct lfs2_config *c, lfs2_block_t block); + + // Sync the state of the underlying block device. Negative error codes + // are propogated to the user. + int (*sync)(const struct lfs2_config *c); + + // Minimum size of a block read. All read operations will be a + // multiple of this value. + lfs2_size_t read_size; + + // Minimum size of a block program. All program operations will be a + // multiple of this value. + lfs2_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. + lfs2_size_t block_size; + + // Number of erasable blocks on the device. + lfs2_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. + lfs2_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. + lfs2_size_t lookahead_size; + + // Optional statically allocated read buffer. Must be cache_size. + // By default lfs2_malloc is used to allocate this buffer. + void *read_buffer; + + // Optional statically allocated program buffer. Must be cache_size. + // By default lfs2_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 lfs2_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 LFS2_NAME_MAX define. Defaults to LFS2_NAME_MAX when zero. Stored in + // superblock and must be respected by other littlefs drivers. + lfs2_size_t name_max; + + // Optional upper limit on files in bytes. No downside for larger files + // but must be <= LFS2_FILE_MAX. Defaults to LFS2_FILE_MAX when zero. Stored + // in superblock and must be respected by other littlefs drivers. + lfs2_size_t file_max; + + // Optional upper limit on custom attributes in bytes. No downside for + // larger attributes size but must be <= LFS2_ATTR_MAX. Defaults to + // LFS2_ATTR_MAX when zero. + lfs2_size_t attr_max; +}; + +// File info structure +struct lfs2_info { + // Type of the file, either LFS2_TYPE_REG or LFS2_TYPE_DIR + uint8_t type; + + // Size of the file, only valid for REG files. Limited to 32-bits. + lfs2_size_t size; + + // Name of the file stored as a null-terminated string. Limited to + // LFS2_NAME_MAX+1, which can be changed by redefining LFS2_NAME_MAX to + // reduce RAM. LFS2_NAME_MAX is stored in superblock and must be + // respected by other littlefs drivers. + char name[LFS2_NAME_MAX+1]; +}; + +// Custom attribute structure, used to describe custom attributes +// committed atomically during file writes. +struct lfs2_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 LFS2_ATTR_MAX + lfs2_size_t size; +}; + +// Optional configuration provided during lfs2_file_opencfg +struct lfs2_file_config { + // Optional statically allocated file buffer. Must be cache_size. + // By default lfs2_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 LFS2_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 lfs2_attr *attrs; + + // Number of custom attributes in the list + lfs2_size_t attr_count; +}; + + +/// internal littlefs data structures /// +typedef struct lfs2_cache { + lfs2_block_t block; + lfs2_off_t off; + lfs2_size_t size; + uint8_t *buffer; +} lfs2_cache_t; + +typedef struct lfs2_mdir { + lfs2_block_t pair[2]; + uint32_t rev; + lfs2_off_t off; + uint32_t etag; + uint16_t count; + bool erased; + bool split; + lfs2_block_t tail[2]; +} lfs2_mdir_t; + +// littlefs directory type +typedef struct lfs2_dir { + struct lfs2_dir *next; + uint16_t id; + uint8_t type; + lfs2_mdir_t m; + + lfs2_off_t pos; + lfs2_block_t head[2]; +} lfs2_dir_t; + +// littlefs file type +typedef struct lfs2_file { + struct lfs2_file *next; + uint16_t id; + uint8_t type; + lfs2_mdir_t m; + + struct lfs2_ctz { + lfs2_block_t head; + lfs2_size_t size; + } ctz; + + uint32_t flags; + lfs2_off_t pos; + lfs2_block_t block; + lfs2_off_t off; + lfs2_cache_t cache; + + const struct lfs2_file_config *cfg; +} lfs2_file_t; + +typedef struct lfs2_superblock { + uint32_t version; + lfs2_size_t block_size; + lfs2_size_t block_count; + lfs2_size_t name_max; + lfs2_size_t file_max; + lfs2_size_t attr_max; +} lfs2_superblock_t; + +typedef struct lfs2_gstate { + uint32_t tag; + lfs2_block_t pair[2]; +} lfs2_gstate_t; + +// The littlefs filesystem type +typedef struct lfs2 { + lfs2_cache_t rcache; + lfs2_cache_t pcache; + + lfs2_block_t root[2]; + struct lfs2_mlist { + struct lfs2_mlist *next; + uint16_t id; + uint8_t type; + lfs2_mdir_t m; + } *mlist; + uint32_t seed; + + lfs2_gstate_t gstate; + lfs2_gstate_t gdisk; + lfs2_gstate_t gdelta; + + struct lfs2_free { + lfs2_block_t off; + lfs2_block_t size; + lfs2_block_t i; + lfs2_block_t ack; + uint32_t *buffer; + } free; + + const struct lfs2_config *cfg; + lfs2_size_t name_max; + lfs2_size_t file_max; + lfs2_size_t attr_max; + +#ifdef LFS2_MIGRATE + struct lfs21 *lfs21; +#endif +} lfs2_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 lfs2_format(lfs2_t *lfs2, const struct lfs2_config *config); + +// Mounts a littlefs +// +// Requires a littlefs object and config struct. Multiple filesystems +// may be mounted simultaneously with multiple littlefs objects. Both +// lfs2 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 lfs2_mount(lfs2_t *lfs2, const struct lfs2_config *config); + +// Unmounts a littlefs +// +// Does nothing besides releasing any allocated resources. +// Returns a negative error code on failure. +int lfs2_unmount(lfs2_t *lfs2); + +/// General operations /// + +// Removes a file or directory +// +// If removing a directory, the directory must be empty. +// Returns a negative error code on failure. +int lfs2_remove(lfs2_t *lfs2, 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 lfs2_rename(lfs2_t *lfs2, 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 lfs2_stat(lfs2_t *lfs2, const char *path, struct lfs2_info *info); + +// Get a custom attribute +// +// Custom attributes are uniquely identified by an 8-bit type and limited +// to LFS2_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 +// LFS2_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. +lfs2_ssize_t lfs2_getattr(lfs2_t *lfs2, const char *path, + uint8_t type, void *buffer, lfs2_size_t size); + +// Set custom attributes +// +// Custom attributes are uniquely identified by an 8-bit type and limited +// to LFS2_ATTR_MAX bytes. If an attribute is not found, it will be +// implicitly created. +// +// Returns a negative error code on failure. +int lfs2_setattr(lfs2_t *lfs2, const char *path, + uint8_t type, const void *buffer, lfs2_size_t size); + +// Removes a custom attribute +// +// If an attribute is not found, nothing happens. +// +// Returns a negative error code on failure. +int lfs2_removeattr(lfs2_t *lfs2, 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 lfs2_open_flags that are bitwise-ored together. +// +// Returns a negative error code on failure. +int lfs2_file_open(lfs2_t *lfs2, lfs2_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 lfs2_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 lfs2_file_opencfg(lfs2_t *lfs2, lfs2_file_t *file, + const char *path, int flags, + const struct lfs2_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 lfs2_file_close(lfs2_t *lfs2, lfs2_file_t *file); + +// Synchronize a file on storage +// +// Any pending writes are written out to storage. +// Returns a negative error code on failure. +int lfs2_file_sync(lfs2_t *lfs2, lfs2_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. +lfs2_ssize_t lfs2_file_read(lfs2_t *lfs2, lfs2_file_t *file, + void *buffer, lfs2_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. +lfs2_ssize_t lfs2_file_write(lfs2_t *lfs2, lfs2_file_t *file, + const void *buffer, lfs2_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. +lfs2_soff_t lfs2_file_seek(lfs2_t *lfs2, lfs2_file_t *file, + lfs2_soff_t off, int whence); + +// Truncates the size of the file to the specified size +// +// Returns a negative error code on failure. +int lfs2_file_truncate(lfs2_t *lfs2, lfs2_file_t *file, lfs2_off_t size); + +// Return the position of the file +// +// Equivalent to lfs2_file_seek(lfs2, file, 0, LFS2_SEEK_CUR) +// Returns the position of the file, or a negative error code on failure. +lfs2_soff_t lfs2_file_tell(lfs2_t *lfs2, lfs2_file_t *file); + +// Change the position of the file to the beginning of the file +// +// Equivalent to lfs2_file_seek(lfs2, file, 0, LFS2_SEEK_SET) +// Returns a negative error code on failure. +int lfs2_file_rewind(lfs2_t *lfs2, lfs2_file_t *file); + +// Return the size of the file +// +// Similar to lfs2_file_seek(lfs2, file, 0, LFS2_SEEK_END) +// Returns the size of the file, or a negative error code on failure. +lfs2_soff_t lfs2_file_size(lfs2_t *lfs2, lfs2_file_t *file); + + +/// Directory operations /// + +// Create a directory +// +// Returns a negative error code on failure. +int lfs2_mkdir(lfs2_t *lfs2, 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 lfs2_dir_open(lfs2_t *lfs2, lfs2_dir_t *dir, const char *path); + +// Close a directory +// +// Releases any allocated resources. +// Returns a negative error code on failure. +int lfs2_dir_close(lfs2_t *lfs2, lfs2_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 lfs2_dir_read(lfs2_t *lfs2, lfs2_dir_t *dir, struct lfs2_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 lfs2_dir_seek(lfs2_t *lfs2, lfs2_dir_t *dir, lfs2_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. +lfs2_soff_t lfs2_dir_tell(lfs2_t *lfs2, lfs2_dir_t *dir); + +// Change the position of the directory to the beginning of the directory +// +// Returns a negative error code on failure. +int lfs2_dir_rewind(lfs2_t *lfs2, lfs2_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. +lfs2_ssize_t lfs2_fs_size(lfs2_t *lfs2); + +// 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 lfs2_fs_traverse(lfs2_t *lfs2, int (*cb)(void*, lfs2_block_t), void *data); + +#ifdef LFS2_MIGRATE +// Attempts to migrate a previous version of littlefs +// +// Behaves similarly to the lfs2_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 lfs2_migrate(lfs2_t *lfs2, const struct lfs2_config *cfg); +#endif + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/features/storage/filesystem/littlefsv2/littlefs/lfs2_util.c b/features/storage/filesystem/littlefsv2/littlefs/lfs2_util.c new file mode 100644 index 00000000000..861e8888787 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/lfs2_util.c @@ -0,0 +1,33 @@ +/* + * lfs2 util functions + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "lfs2_util.h" + +// Only compile if user does not provide custom config +#ifndef LFS2_CONFIG +#ifndef __MBED__ + +// Software CRC implementation with small lookup table +uint32_t lfs2_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 +#endif diff --git a/features/storage/filesystem/littlefsv2/littlefs/lfs2_util.h b/features/storage/filesystem/littlefsv2/littlefs/lfs2_util.h new file mode 100644 index 00000000000..b36a2034258 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/lfs2_util.h @@ -0,0 +1,283 @@ +/* + * lfs2 utility functions + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS2_UTIL_H +#define LFS2_UTIL_H + +// Users can override lfs2_util.h with their own configuration by defining +// LFS2_CONFIG as a header file to include (-DLFS2_CONFIG=lfs2_config.h). +// +// If LFS2_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 lfs2_util.h +// and modifying as needed. +#ifdef LFS2_CONFIG +#define LFS2_STRINGIZE(x) LFS2_STRINGIZE2(x) +#define LFS2_STRINGIZE2(x) #x +#include LFS2_STRINGIZE(LFS2_CONFIG) +#else + +// System includes +#include +#include +#include +#include + +#ifndef LFS2_NO_MALLOC +#include +#endif +#ifndef LFS2_NO_ASSERT +#include +#endif +#if !defined(LFS2_NO_DEBUG) || \ + !defined(LFS2_NO_WARN) || \ + !defined(LFS2_NO_ERROR) || \ + defined(LFS2_YES_TRACE) +#include +#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 + +#ifdef __MBED__ +#include "mbed_debug.h" +#include "mbed_assert.h" +#include "cmsis_compiler.h" +#else +#define MBED_LFS2_ENABLE_INFO false +#define MBED_LFS2_ENABLE_DEBUG true +#define MBED_LFS2_ENABLE_WARN true +#define MBED_LFS2_ENABLE_ERROR true +#define MBED_LFS2_ENABLE_ASSERT true +#define MBED_LFS2_INTRINSICS true +#endif + +// Logging functions +#if defined(LFS2_YES_TRACE) && MBED_LFS2_ENABLE_TRACE +#define LFS2_TRACE_(fmt, ...) \ + printf("%s:%d:trace: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS2_TRACE(...) LFS2_TRACE_(__VA_ARGS__, "") +#elif defined(LFS2_YES_TRACE) && !defined(MBED_LFS2_ENABLE_TRACE) +#define LFS2_TRACE_(fmt, ...) \ + debug("%s:%d:trace: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS2_TRACE(...) LFS2_TRACE_(__VA_ARGS__, "") +#else +#define LFS2_TRACE(...) +#endif + +#if !defined(LFS2_NO_DEBUG) && MBED_LFS2_ENABLE_DEBUG +#define LFS2_DEBUG_(fmt, ...) \ + printf("%s:%d:debug: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS2_DEBUG(...) LFS2_DEBUG_(__VA_ARGS__, "") +#elif !defined(LFS2_NO_DEBUG) && !defined(MBED_LFS2_ENABLE_DEBUG) +#define LFS2_DEBUG_(fmt, ...) \ + debug("%s:%d:debug: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS2_DEBUG(...) LFS2_DEBUG_(__VA_ARGS__, "") +#else +#define LFS2_DEBUG(...) +#endif + +#if !defined(LFS2_NO_WARN) && MBED_LFS2_ENABLE_WARN +#define LFS2_WARN_(fmt, ...) \ + printf("%s:%d:warn: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS2_WARN(...) LFS2_WARN_(__VA_ARGS__, "") +#elif !defined(LFS2_NO_WARN) && !defined(MBED_LFS2_ENABLE_WARN) +#define LFS2_WARN_(fmt, ...) \ + debug("%s:%d:warn: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS2_WARN(...) LFS2_WARN_(__VA_ARGS__, "") +#else +#define LFS2_WARN(...) +#endif + +#if !defined(LFS2_NO_ERROR) && MBED_LFS2_ENABLE_ERROR +#define LFS2_ERROR_(fmt, ...) \ + printf("%s:%d:error: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS2_ERROR(...) LFS2_ERROR_(__VA_ARGS__, "") +#elif !defined(LFS2_NO_ERROR) && !defined(MBED_LFS2_ENABLE_ERROR) +#define LFS2_ERROR_(fmt, ...) \ + debug("%s:%d:error: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS2_ERROR(...) LFS2_ERROR_(__VA_ARGS__, "") +#else +#define LFS2_ERROR(...) +#endif + +// Runtime assertions +#if !defined(LFS2_NO_ASSERT) && MBED_LFS2_ENABLE_ASSERT +#define LFS2_ASSERT(test) assert(test) +#elif !defined(LFS2_NO_ASSERT) && !defined(MBED_LFS2_ENABLE_ASSERT) +#define LFS2_ASSERT(test) MBED_ASSERT(test) +#else +#define LFS2_ASSERT(test) +#endif + + +// Builtin functions, these may be replaced by more efficient +// toolchain-specific implementations. LFS2_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 lfs2_max(uint32_t a, uint32_t b) { + return (a > b) ? a : b; +} + +static inline uint32_t lfs2_min(uint32_t a, uint32_t b) { + return (a < b) ? a : b; +} + +// Align to nearest multiple of a size +static inline uint32_t lfs2_aligndown(uint32_t a, uint32_t alignment) { + return a - (a % alignment); +} + +static inline uint32_t lfs2_alignup(uint32_t a, uint32_t alignment) { + return lfs2_aligndown(a + alignment-1, alignment); +} + +// Find the smallest power of 2 greater than or equal to a +static inline uint32_t lfs2_npw2(uint32_t a) { +#if !defined(LFS2_NO_INTRINSICS) && MBED_LFS2_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 +// lfs2_ctz(0) may be undefined +static inline uint32_t lfs2_ctz(uint32_t a) { +#if !defined(LFS2_NO_INTRINSICS) && MBED_LFS2_INTRINSICS && \ + defined(__GNUC__) + return __builtin_ctz(a); +#else + return lfs2_npw2((a & -a) + 1) - 1; +#endif +} + +// Count the number of binary ones in a +static inline uint32_t lfs2_popc(uint32_t a) { +#if !defined(LFS2_NO_INTRINSICS) && MBED_LFS2_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 lfs2_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 lfs2_fromle32(uint32_t a) { +#if !defined(LFS2_NO_INTRINSICS) && MBED_LFS2_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(LFS2_NO_INTRINSICS) && MBED_LFS2_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 lfs2_tole32(uint32_t a) { + return lfs2_fromle32(a); +} + +// Reverse the bits in a +static inline uint32_t lfs2_rbit(uint32_t a) { +#if !defined(LFS2_NO_INTRINSICS) && MBED_LFS2_INTRINSICS && \ + defined(__MBED__) + return __RBIT(a); +#else + a = ((a & 0xaaaaaaaa) >> 1) | ((a & 0x55555555) << 1); + a = ((a & 0xcccccccc) >> 2) | ((a & 0x33333333) << 2); + a = ((a & 0xf0f0f0f0) >> 4) | ((a & 0x0f0f0f0f) << 4); + a = ((a & 0xff00ff00) >> 8) | ((a & 0x00ff00ff) << 8); + a = (a >> 16) | (a << 16); + return a; +#endif +} + +// Convert between 32-bit big-endian and native order +static inline uint32_t lfs2_frombe32(uint32_t a) { +#if !defined(LFS2_NO_INTRINSICS) && MBED_LFS2_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(LFS2_NO_INTRINSICS) && MBED_LFS2_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 lfs2_tobe32(uint32_t a) { + return lfs2_frombe32(a); +} + +// Calculate CRC-32 with polynomial = 0x04c11db7 +uint32_t lfs2_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 *lfs2_malloc(size_t size) { +#ifndef LFS2_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 lfs2_free(void *p) { +#ifndef LFS2_NO_MALLOC + free(p); +#else + (void)p; +#endif +} + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif +#endif diff --git a/features/storage/filesystem/littlefsv2/littlefs/scripts/explode_asserts.py b/features/storage/filesystem/littlefsv2/littlefs/scripts/explode_asserts.py new file mode 100755 index 00000000000..e1942f6b03c --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/scripts/explode_asserts.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 + +import re +import sys + +PATTERN = ['LFS2_ASSERT', 'assert'] +PREFIX = 'LFS2' +MAXWIDTH = 16 + +ASSERT = "__{PREFIX}_ASSERT_{TYPE}_{COMP}" +FAIL = """ +__attribute__((unused)) +static void __{prefix}_assert_fail_{type}( + const char *file, int line, const char *comp, + {ctype} lh, size_t lsize, + {ctype} rh, size_t rsize) {{ + printf("%s:%d:assert: assert failed with ", file, line); + __{prefix}_assert_print_{type}(lh, lsize); + printf(", expected %s ", comp); + __{prefix}_assert_print_{type}(rh, rsize); + printf("\\n"); + fflush(NULL); + raise(SIGABRT); +}} +""" + +COMP = { + '==': 'eq', + '!=': 'ne', + '<=': 'le', + '>=': 'ge', + '<': 'lt', + '>': 'gt', +} + +TYPE = { + 'int': { + 'ctype': 'intmax_t', + 'fail': FAIL, + 'print': """ + __attribute__((unused)) + static void __{prefix}_assert_print_{type}({ctype} v, size_t size) {{ + (void)size; + printf("%"PRIiMAX, v); + }} + """, + 'assert': """ + #define __{PREFIX}_ASSERT_{TYPE}_{COMP}(file, line, lh, rh) + do {{ + __typeof__(lh) _lh = lh; + __typeof__(lh) _rh = (__typeof__(lh))rh; + if (!(_lh {op} _rh)) {{ + __{prefix}_assert_fail_{type}(file, line, "{comp}", + (intmax_t)_lh, 0, (intmax_t)_rh, 0); + }} + }} while (0) + """ + }, + 'bool': { + 'ctype': 'bool', + 'fail': FAIL, + 'print': """ + __attribute__((unused)) + static void __{prefix}_assert_print_{type}({ctype} v, size_t size) {{ + (void)size; + printf("%s", v ? "true" : "false"); + }} + """, + 'assert': """ + #define __{PREFIX}_ASSERT_{TYPE}_{COMP}(file, line, lh, rh) + do {{ + bool _lh = !!(lh); + bool _rh = !!(rh); + if (!(_lh {op} _rh)) {{ + __{prefix}_assert_fail_{type}(file, line, "{comp}", + _lh, 0, _rh, 0); + }} + }} while (0) + """ + }, + 'mem': { + 'ctype': 'const void *', + 'fail': FAIL, + 'print': """ + __attribute__((unused)) + static void __{prefix}_assert_print_{type}({ctype} v, size_t size) {{ + const uint8_t *s = v; + printf("\\\""); + for (size_t i = 0; i < size && i < {maxwidth}; i++) {{ + if (s[i] >= ' ' && s[i] <= '~') {{ + printf("%c", s[i]); + }} else {{ + printf("\\\\x%02x", s[i]); + }} + }} + if (size > {maxwidth}) {{ + printf("..."); + }} + printf("\\\""); + }} + """, + 'assert': """ + #define __{PREFIX}_ASSERT_{TYPE}_{COMP}(file, line, lh, rh, size) + do {{ + const void *_lh = lh; + const void *_rh = rh; + if (!(memcmp(_lh, _rh, size) {op} 0)) {{ + __{prefix}_assert_fail_{type}(file, line, "{comp}", + _lh, size, _rh, size); + }} + }} while (0) + """ + }, + 'str': { + 'ctype': 'const char *', + 'fail': FAIL, + 'print': """ + __attribute__((unused)) + static void __{prefix}_assert_print_{type}({ctype} v, size_t size) {{ + __{prefix}_assert_print_mem(v, size); + }} + """, + 'assert': """ + #define __{PREFIX}_ASSERT_{TYPE}_{COMP}(file, line, lh, rh) + do {{ + const char *_lh = lh; + const char *_rh = rh; + if (!(strcmp(_lh, _rh) {op} 0)) {{ + __{prefix}_assert_fail_{type}(file, line, "{comp}", + _lh, strlen(_lh), _rh, strlen(_rh)); + }} + }} while (0) + """ + } +} + +def mkdecls(outf, maxwidth=16): + outf.write("#include \n") + outf.write("#include \n") + outf.write("#include \n") + outf.write("#include \n") + outf.write("#include \n") + + for type, desc in sorted(TYPE.items()): + format = { + 'type': type.lower(), 'TYPE': type.upper(), + 'ctype': desc['ctype'], + 'prefix': PREFIX.lower(), 'PREFIX': PREFIX.upper(), + 'maxwidth': maxwidth, + } + outf.write(re.sub('\s+', ' ', + desc['print'].strip().format(**format))+'\n') + outf.write(re.sub('\s+', ' ', + desc['fail'].strip().format(**format))+'\n') + + for op, comp in sorted(COMP.items()): + format.update({ + 'comp': comp.lower(), 'COMP': comp.upper(), + 'op': op, + }) + outf.write(re.sub('\s+', ' ', + desc['assert'].strip().format(**format))+'\n') + +def mkassert(type, comp, lh, rh, size=None): + format = { + 'type': type.lower(), 'TYPE': type.upper(), + 'comp': comp.lower(), 'COMP': comp.upper(), + 'prefix': PREFIX.lower(), 'PREFIX': PREFIX.upper(), + 'lh': lh.strip(' '), + 'rh': rh.strip(' '), + 'size': size, + } + if size: + return ((ASSERT + '(__FILE__, __LINE__, {lh}, {rh}, {size})') + .format(**format)) + else: + return ((ASSERT + '(__FILE__, __LINE__, {lh}, {rh})') + .format(**format)) + + +# simple recursive descent parser +LEX = { + 'ws': [r'(?:\s|\n|#.*?\n|//.*?\n|/\*.*?\*/)+'], + 'assert': PATTERN, + 'string': [r'"(?:\\.|[^"])*"', r"'(?:\\.|[^'])\'"], + 'arrow': ['=>'], + 'paren': ['\(', '\)'], + 'op': ['strcmp', 'memcmp', '->'], + 'comp': ['==', '!=', '<=', '>=', '<', '>'], + 'logic': ['\&\&', '\|\|'], + 'sep': [':', ';', '\{', '\}', ','], +} + +class ParseFailure(Exception): + def __init__(self, expected, found): + self.expected = expected + self.found = found + + def __str__(self): + return "expected %r, found %s..." % ( + self.expected, repr(self.found)[:70]) + +class Parse: + def __init__(self, inf, lexemes): + p = '|'.join('(?P<%s>%s)' % (n, '|'.join(l)) + for n, l in lexemes.items()) + p = re.compile(p, re.DOTALL) + data = inf.read() + tokens = [] + while True: + m = p.search(data) + if m: + if m.start() > 0: + tokens.append((None, data[:m.start()])) + tokens.append((m.lastgroup, m.group())) + data = data[m.end():] + else: + tokens.append((None, data)) + break + self.tokens = tokens + self.off = 0 + + def lookahead(self, *pattern): + if self.off < len(self.tokens): + token = self.tokens[self.off] + if token[0] in pattern or token[1] in pattern: + self.m = token[1] + return self.m + self.m = None + return self.m + + def accept(self, *patterns): + m = self.lookahead(*patterns) + if m is not None: + self.off += 1 + return m + + def expect(self, *patterns): + m = self.accept(*patterns) + if not m: + raise ParseFailure(patterns, self.tokens[self.off:]) + return m + + def push(self): + return self.off + + def pop(self, state): + self.off = state + +def passert(p): + def pastr(p): + p.expect('assert') ; p.accept('ws') ; p.expect('(') ; p.accept('ws') + p.expect('strcmp') ; p.accept('ws') ; p.expect('(') ; p.accept('ws') + lh = pexpr(p) ; p.accept('ws') + p.expect(',') ; p.accept('ws') + rh = pexpr(p) ; p.accept('ws') + p.expect(')') ; p.accept('ws') + comp = p.expect('comp') ; p.accept('ws') + p.expect('0') ; p.accept('ws') + p.expect(')') + return mkassert('str', COMP[comp], lh, rh) + + def pamem(p): + p.expect('assert') ; p.accept('ws') ; p.expect('(') ; p.accept('ws') + p.expect('memcmp') ; p.accept('ws') ; p.expect('(') ; p.accept('ws') + lh = pexpr(p) ; p.accept('ws') + p.expect(',') ; p.accept('ws') + rh = pexpr(p) ; p.accept('ws') + p.expect(',') ; p.accept('ws') + size = pexpr(p) ; p.accept('ws') + p.expect(')') ; p.accept('ws') + comp = p.expect('comp') ; p.accept('ws') + p.expect('0') ; p.accept('ws') + p.expect(')') + return mkassert('mem', COMP[comp], lh, rh, size) + + def paint(p): + p.expect('assert') ; p.accept('ws') ; p.expect('(') ; p.accept('ws') + lh = pexpr(p) ; p.accept('ws') + comp = p.expect('comp') ; p.accept('ws') + rh = pexpr(p) ; p.accept('ws') + p.expect(')') + return mkassert('int', COMP[comp], lh, rh) + + def pabool(p): + p.expect('assert') ; p.accept('ws') ; p.expect('(') ; p.accept('ws') + lh = pexprs(p) ; p.accept('ws') + p.expect(')') + return mkassert('bool', 'eq', lh, 'true') + + def pa(p): + return p.expect('assert') + + state = p.push() + lastf = None + for pa in [pastr, pamem, paint, pabool, pa]: + try: + return pa(p) + except ParseFailure as f: + p.pop(state) + lastf = f + else: + raise lastf + +def pexpr(p): + res = [] + while True: + if p.accept('('): + res.append(p.m) + while True: + res.append(pexprs(p)) + if p.accept('sep'): + res.append(p.m) + else: + break + res.append(p.expect(')')) + elif p.lookahead('assert'): + res.append(passert(p)) + elif p.accept('assert', 'ws', 'string', 'op', None): + res.append(p.m) + else: + return ''.join(res) + +def pexprs(p): + res = [] + while True: + res.append(pexpr(p)) + if p.accept('comp', 'logic', ','): + res.append(p.m) + else: + return ''.join(res) + +def pstmt(p): + ws = p.accept('ws') or '' + lh = pexprs(p) + if p.accept('=>'): + rh = pexprs(p) + return ws + mkassert('int', 'eq', lh, rh) + else: + return ws + lh + + +def main(args): + inf = open(args.input, 'r') if args.input else sys.stdin + outf = open(args.output, 'w') if args.output else sys.stdout + + lexemes = LEX.copy() + if args.pattern: + lexemes['assert'] = args.pattern + p = Parse(inf, lexemes) + + # write extra verbose asserts + mkdecls(outf, maxwidth=args.maxwidth) + if args.input: + outf.write("#line %d \"%s\"\n" % (1, args.input)) + + # parse and write out stmt at a time + try: + while True: + outf.write(pstmt(p)) + if p.accept('sep'): + outf.write(p.m) + else: + break + except ParseFailure as f: + pass + + for i in range(p.off, len(p.tokens)): + outf.write(p.tokens[i][1]) + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser( + description="Cpp step that increases assert verbosity") + parser.add_argument('input', nargs='?', + help="Input C file after cpp.") + parser.add_argument('-o', '--output', required=True, + help="Output C file.") + parser.add_argument('-p', '--pattern', action='append', + help="Patterns to search for starting an assert statement.") + parser.add_argument('--maxwidth', default=MAXWIDTH, type=int, + help="Maximum number of characters to display for strcmp and memcmp.") + main(parser.parse_args()) diff --git a/features/storage/filesystem/littlefsv2/littlefs/scripts/prefix.py b/features/storage/filesystem/littlefsv2/littlefs/scripts/prefix.py new file mode 100755 index 00000000000..434320b6896 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/scripts/prefix.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python2 + +# This script replaces prefixes of files, and symbols in that file. +# Useful for creating different versions of the codebase that don't +# conflict at compile time. +# +# example: +# $ ./scripts/prefix.py lfs22 + +import os +import os.path +import re +import glob +import itertools +import tempfile +import shutil +import subprocess + +DEFAULT_PREFIX = "lfs2" + +def subn(from_prefix, to_prefix, name): + name, count1 = re.subn('\\b'+from_prefix, to_prefix, name) + name, count2 = re.subn('\\b'+from_prefix.upper(), to_prefix.upper(), name) + name, count3 = re.subn('\\B-D'+from_prefix.upper(), + '-D'+to_prefix.upper(), name) + return name, count1+count2+count3 + +def main(from_prefix, to_prefix=None, files=None): + if not to_prefix: + from_prefix, to_prefix = DEFAULT_PREFIX, from_prefix + + if not files: + files = subprocess.check_output([ + 'git', 'ls-tree', '-r', '--name-only', 'HEAD']).split() + + for oldname in files: + # Rename any matching file names + newname, namecount = subn(from_prefix, to_prefix, oldname) + if namecount: + subprocess.check_call(['git', 'mv', oldname, newname]) + + # Rename any prefixes in file + count = 0 + with open(newname+'~', 'w') as tempf: + with open(newname) as newf: + for line in newf: + line, n = subn(from_prefix, to_prefix, line) + count += n + tempf.write(line) + shutil.copystat(newname, newname+'~') + os.rename(newname+'~', newname) + subprocess.check_call(['git', 'add', newname]) + + # Summary + print '%s: %d replacements' % ( + '%s -> %s' % (oldname, newname) if namecount else oldname, + count) + +if __name__ == "__main__": + import sys + sys.exit(main(*sys.argv[1:])) diff --git a/features/storage/filesystem/littlefsv2/littlefs/scripts/readblock.py b/features/storage/filesystem/littlefsv2/littlefs/scripts/readblock.py new file mode 100755 index 00000000000..817517bc46d --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/scripts/readblock.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import subprocess as sp + +def main(args): + with open(args.disk, 'rb') as f: + f.seek(args.block * args.block_size) + block = (f.read(args.block_size) + .ljust(args.block_size, b'\xff')) + + # what did you expect? + print("%-8s %-s" % ('off', 'data')) + return sp.run(['xxd', '-g1', '-'], input=block).returncode + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Hex dump a specific block in a disk.") + parser.add_argument('disk', + help="File representing the block device.") + parser.add_argument('block_size', type=lambda x: int(x, 0), + help="Size of a block in bytes.") + parser.add_argument('block', type=lambda x: int(x, 0), + help="Address of block to dump.") + sys.exit(main(parser.parse_args())) diff --git a/features/storage/filesystem/littlefsv2/littlefs/scripts/readmdir.py b/features/storage/filesystem/littlefsv2/littlefs/scripts/readmdir.py new file mode 100755 index 00000000000..b6c3dcca1f7 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/scripts/readmdir.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 + +import struct +import binascii +import sys +import itertools as it + +TAG_TYPES = { + 'splice': (0x700, 0x400), + 'create': (0x7ff, 0x401), + 'delete': (0x7ff, 0x4ff), + 'name': (0x700, 0x000), + 'reg': (0x7ff, 0x001), + 'dir': (0x7ff, 0x002), + 'superblock': (0x7ff, 0x0ff), + 'struct': (0x700, 0x200), + 'dirstruct': (0x7ff, 0x200), + 'ctzstruct': (0x7ff, 0x202), + 'inlinestruct': (0x7ff, 0x201), + 'userattr': (0x700, 0x300), + 'tail': (0x700, 0x600), + 'softtail': (0x7ff, 0x600), + 'hardtail': (0x7ff, 0x601), + 'gstate': (0x700, 0x700), + 'movestate': (0x7ff, 0x7ff), + 'crc': (0x700, 0x500), +} + +class Tag: + def __init__(self, *args): + if len(args) == 1: + self.tag = args[0] + elif len(args) == 3: + if isinstance(args[0], str): + type = TAG_TYPES[args[0]][1] + else: + type = args[0] + + if isinstance(args[1], str): + id = int(args[1], 0) if args[1] not in 'x.' else 0x3ff + else: + id = args[1] + + if isinstance(args[2], str): + size = int(args[2], str) if args[2] not in 'x.' else 0x3ff + else: + size = args[2] + + self.tag = (type << 20) | (id << 10) | size + else: + assert False + + @property + def isvalid(self): + return not bool(self.tag & 0x80000000) + + @property + def isattr(self): + return not bool(self.tag & 0x40000000) + + @property + def iscompactable(self): + return bool(self.tag & 0x20000000) + + @property + def isunique(self): + return not bool(self.tag & 0x10000000) + + @property + def type(self): + return (self.tag & 0x7ff00000) >> 20 + + @property + def type1(self): + return (self.tag & 0x70000000) >> 20 + + @property + def type3(self): + return (self.tag & 0x7ff00000) >> 20 + + @property + def id(self): + return (self.tag & 0x000ffc00) >> 10 + + @property + def size(self): + return (self.tag & 0x000003ff) >> 0 + + @property + def dsize(self): + return 4 + (self.size if self.size != 0x3ff else 0) + + @property + def chunk(self): + return self.type & 0xff + + @property + def schunk(self): + return struct.unpack('b', struct.pack('B', self.chunk))[0] + + def is_(self, type): + return (self.type & TAG_TYPES[type][0]) == TAG_TYPES[type][1] + + def mkmask(self): + return Tag( + 0x700 if self.isunique else 0x7ff, + 0x3ff if self.isattr else 0, + 0) + + def chid(self, nid): + ntag = Tag(self.type, nid, self.size) + if hasattr(self, 'off'): ntag.off = self.off + if hasattr(self, 'data'): ntag.data = self.data + if hasattr(self, 'crc'): ntag.crc = self.crc + return ntag + + def typerepr(self): + if self.is_('crc') and getattr(self, 'crc', 0xffffffff) != 0xffffffff: + return 'crc (bad)' + + reverse_types = {v: k for k, v in TAG_TYPES.items()} + for prefix in range(12): + mask = 0x7ff & ~((1 << prefix)-1) + if (mask, self.type & mask) in reverse_types: + type = reverse_types[mask, self.type & mask] + if prefix > 0: + return '%s %#0*x' % ( + type, prefix//4, self.type & ((1 << prefix)-1)) + else: + return type + else: + return '%02x' % self.type + + def idrepr(self): + return repr(self.id) if self.id != 0x3ff else '.' + + def sizerepr(self): + return repr(self.size) if self.size != 0x3ff else 'x' + + def __repr__(self): + return 'Tag(%r, %d, %d)' % (self.typerepr(), self.id, self.size) + + def __lt__(self, other): + return (self.id, self.type) < (other.id, other.type) + + def __bool__(self): + return self.isvalid + + def __int__(self): + return self.tag + + def __index__(self): + return self.tag + +class MetadataPair: + def __init__(self, blocks): + if len(blocks) > 1: + self.pair = [MetadataPair([block]) for block in blocks] + self.pair = sorted(self.pair, reverse=True) + + self.data = self.pair[0].data + self.rev = self.pair[0].rev + self.tags = self.pair[0].tags + self.ids = self.pair[0].ids + self.log = self.pair[0].log + self.all_ = self.pair[0].all_ + return + + self.pair = [self] + self.data = blocks[0] + block = self.data + + self.rev, = struct.unpack('= 4: + ntag, = struct.unpack('>I', block[off:off+4]) + + tag = Tag(int(tag) ^ ntag) + tag.off = off + 4 + tag.data = block[off+4:off+tag.dsize] + if tag.is_('crc'): + crc = binascii.crc32(block[off:off+4+4], crc) + else: + crc = binascii.crc32(block[off:off+tag.dsize], crc) + tag.crc = crc + off += tag.dsize + + self.all_.append(tag) + + if tag.is_('crc'): + # is valid commit? + if crc != 0xffffffff: + corrupt = True + if not corrupt: + self.log = self.all_.copy() + + # reset tag parsing + crc = 0 + tag = Tag(int(tag) ^ ((tag.type & 1) << 31)) + + # find active ids + self.ids = list(it.takewhile( + lambda id: Tag('name', id, 0) in self, + it.count())) + + # find most recent tags + self.tags = [] + for tag in self.log: + if tag.is_('crc') or tag.is_('splice'): + continue + elif tag.id == 0x3ff: + if tag in self and self[tag] is tag: + self.tags.append(tag) + else: + # id could have change, I know this is messy and slow + # but it works + for id in self.ids: + ntag = tag.chid(id) + if ntag in self and self[ntag] is tag: + self.tags.append(ntag) + + self.tags = sorted(self.tags) + + def __bool__(self): + return bool(self.log) + + def __lt__(self, other): + # corrupt blocks don't count + if not self or not other: + return bool(other) + + # use sequence arithmetic to avoid overflow + return not ((other.rev - self.rev) & 0x80000000) + + def __contains__(self, args): + try: + self[args] + return True + except KeyError: + return False + + def __getitem__(self, args): + if isinstance(args, tuple): + gmask, gtag = args + else: + gmask, gtag = args.mkmask(), args + + gdiff = 0 + for tag in reversed(self.log): + if (gmask.id != 0 and tag.is_('splice') and + tag.id <= gtag.id - gdiff): + if tag.is_('create') and tag.id == gtag.id - gdiff: + # creation point + break + + gdiff += tag.schunk + + if ((int(gmask) & int(tag)) == + (int(gmask) & int(gtag.chid(gtag.id - gdiff)))): + if tag.size == 0x3ff: + # deleted + break + + return tag + + raise KeyError(gmask, gtag) + + def _dump_tags(self, tags, f=sys.stdout, truncate=True): + f.write("%-8s %-8s %-13s %4s %4s" % ( + 'off', 'tag', 'type', 'id', 'len')) + if truncate: + f.write(' data (truncated)') + f.write('\n') + + for tag in tags: + f.write("%08x: %08x %-13s %4s %4s" % ( + tag.off, tag, + tag.typerepr(), tag.idrepr(), tag.sizerepr())) + if truncate: + f.write(" %-23s %-8s\n" % ( + ' '.join('%02x' % c for c in tag.data[:8]), + ''.join(c if c >= ' ' and c <= '~' else '.' + for c in map(chr, tag.data[:8])))) + else: + f.write("\n") + for i in range(0, len(tag.data), 16): + f.write(" %08x: %-47s %-16s\n" % ( + tag.off+i, + ' '.join('%02x' % c for c in tag.data[i:i+16]), + ''.join(c if c >= ' ' and c <= '~' else '.' + for c in map(chr, tag.data[i:i+16])))) + + def dump_tags(self, f=sys.stdout, truncate=True): + self._dump_tags(self.tags, f=f, truncate=truncate) + + def dump_log(self, f=sys.stdout, truncate=True): + self._dump_tags(self.log, f=f, truncate=truncate) + + def dump_all(self, f=sys.stdout, truncate=True): + self._dump_tags(self.all_, f=f, truncate=truncate) + +def main(args): + blocks = [] + with open(args.disk, 'rb') as f: + for block in [args.block1, args.block2]: + if block is None: + continue + f.seek(block * args.block_size) + blocks.append(f.read(args.block_size) + .ljust(args.block_size, b'\xff')) + + # find most recent pair + mdir = MetadataPair(blocks) + + try: + mdir.tail = mdir[Tag('tail', 0, 0)] + if mdir.tail.size != 8 or mdir.tail.data == 8*b'\xff': + mdir.tail = None + except KeyError: + mdir.tail = None + + print("mdir {%s} rev %d%s%s%s" % ( + ', '.join('%#x' % b + for b in [args.block1, args.block2] + if b is not None), + mdir.rev, + ' (was %s)' % ', '.join('%d' % m.rev for m in mdir.pair[1:]) + if len(mdir.pair) > 1 else '', + ' (corrupted!)' if not mdir else '', + ' -> {%#x, %#x}' % struct.unpack('=%d" % max(tag.size, 1)) + if tag.type: + print(" move dir {%#x, %#x} id %d" % ( + blocks[0], blocks[1], tag.id)) + + # print mdir info + for i, dir in enumerate(dirs): + print("dir %s" % (json.dumps(dir[0].path) + if hasattr(dir[0], 'path') else '(orphan)')) + + for j, mdir in enumerate(dir): + print("mdir {%#x, %#x} rev %d (was %d)%s%s" % ( + mdir.blocks[0], mdir.blocks[1], mdir.rev, mdir.pair[1].rev, + ' (corrupted!)' if not mdir else '', + ' -> {%#x, %#x}' % struct.unpack(' +extern const char *lfs2_testbd_path; +extern uint32_t lfs2_testbd_cycles; +""" +DEFINES = { + 'LFS2_READ_SIZE': 16, + 'LFS2_PROG_SIZE': 'LFS2_READ_SIZE', + 'LFS2_BLOCK_SIZE': 512, + 'LFS2_BLOCK_COUNT': 1024, + 'LFS2_BLOCK_CYCLES': -1, + 'LFS2_CACHE_SIZE': '(64 % LFS2_PROG_SIZE == 0 ? 64 : LFS2_PROG_SIZE)', + 'LFS2_LOOKAHEAD_SIZE': 16, + 'LFS2_ERASE_VALUE': 0xff, + 'LFS2_ERASE_CYCLES': 0, + 'LFS2_BADBLOCK_BEHAVIOR': 'LFS2_TESTBD_BADBLOCK_PROGERROR', +} +PROLOGUE = """ + // prologue + __attribute__((unused)) lfs2_t lfs2; + __attribute__((unused)) lfs2_testbd_t bd; + __attribute__((unused)) lfs2_file_t file; + __attribute__((unused)) lfs2_dir_t dir; + __attribute__((unused)) struct lfs2_info info; + __attribute__((unused)) char path[1024]; + __attribute__((unused)) uint8_t buffer[1024]; + __attribute__((unused)) lfs2_size_t size; + __attribute__((unused)) int err; + + __attribute__((unused)) const struct lfs2_config cfg = { + .context = &bd, + .read = lfs2_testbd_read, + .prog = lfs2_testbd_prog, + .erase = lfs2_testbd_erase, + .sync = lfs2_testbd_sync, + .read_size = LFS2_READ_SIZE, + .prog_size = LFS2_PROG_SIZE, + .block_size = LFS2_BLOCK_SIZE, + .block_count = LFS2_BLOCK_COUNT, + .block_cycles = LFS2_BLOCK_CYCLES, + .cache_size = LFS2_CACHE_SIZE, + .lookahead_size = LFS2_LOOKAHEAD_SIZE, + }; + + __attribute__((unused)) const struct lfs2_testbd_config bdcfg = { + .erase_value = LFS2_ERASE_VALUE, + .erase_cycles = LFS2_ERASE_CYCLES, + .badblock_behavior = LFS2_BADBLOCK_BEHAVIOR, + .power_cycles = lfs2_testbd_cycles, + }; + + lfs2_testbd_createcfg(&cfg, lfs2_testbd_path, &bdcfg) => 0; +""" +EPILOGUE = """ + // epilogue + lfs2_testbd_destroy(&cfg) => 0; +""" +PASS = '\033[32m✓\033[0m' +FAIL = '\033[31m✗\033[0m' + +class TestFailure(Exception): + def __init__(self, case, returncode=None, stdout=None, assert_=None): + self.case = case + self.returncode = returncode + self.stdout = stdout + self.assert_ = assert_ + +class TestCase: + def __init__(self, config, filter=filter, + suite=None, caseno=None, lineno=None, **_): + self.config = config + self.filter = filter + self.suite = suite + self.caseno = caseno + self.lineno = lineno + + self.code = config['code'] + self.code_lineno = config['code_lineno'] + self.defines = config.get('define', {}) + self.if_ = config.get('if', None) + self.in_ = config.get('in', None) + + def __str__(self): + if hasattr(self, 'permno'): + if any(k not in self.case.defines for k in self.defines): + return '%s#%d#%d (%s)' % ( + self.suite.name, self.caseno, self.permno, ', '.join( + '%s=%s' % (k, v) for k, v in self.defines.items() + if k not in self.case.defines)) + else: + return '%s#%d#%d' % ( + self.suite.name, self.caseno, self.permno) + else: + return '%s#%d' % ( + self.suite.name, self.caseno) + + def permute(self, class_=None, defines={}, permno=None, **_): + ncase = (class_ or type(self))(self.config) + for k, v in self.__dict__.items(): + setattr(ncase, k, v) + ncase.case = self + ncase.perms = [ncase] + ncase.permno = permno + ncase.defines = defines + return ncase + + def build(self, f, **_): + # prologue + for k, v in sorted(self.defines.items()): + if k not in self.suite.defines: + f.write('#define %s %s\n' % (k, v)) + + f.write('void test_case%d(%s) {' % (self.caseno, ','.join( + '\n'+8*' '+'__attribute__((unused)) intmax_t %s' % k + for k in sorted(self.perms[0].defines) + if k not in self.defines))) + + f.write(PROLOGUE) + f.write('\n') + f.write(4*' '+'// test case %d\n' % self.caseno) + f.write(4*' '+'#line %d "%s"\n' % (self.code_lineno, self.suite.path)) + + # test case goes here + f.write(self.code) + + # epilogue + f.write(EPILOGUE) + f.write('}\n') + + for k, v in sorted(self.defines.items()): + if k not in self.suite.defines: + f.write('#undef %s\n' % k) + + def shouldtest(self, **args): + if (self.filter is not None and + len(self.filter) >= 1 and + self.filter[0] != self.caseno): + return False + elif (self.filter is not None and + len(self.filter) >= 2 and + self.filter[1] != self.permno): + return False + elif args.get('no_internal', False) and self.in_ is not None: + return False + elif self.if_ is not None: + if_ = self.if_ + while True: + for k, v in sorted(self.defines.items(), + key=lambda x: len(x[0]), reverse=True): + if k in if_: + if_ = if_.replace(k, '(%s)' % v) + break + else: + break + if_ = ( + re.sub('(\&\&|\?)', ' and ', + re.sub('(\|\||:)', ' or ', + re.sub('!(?!=)', ' not ', if_)))) + return eval(if_) + else: + return True + + def test(self, exec=[], persist=False, cycles=None, + gdb=False, failure=None, disk=None, **args): + # build command + cmd = exec + ['./%s.test' % self.suite.path, + repr(self.caseno), repr(self.permno)] + + # persist disk or keep in RAM for speed? + if persist: + if not disk: + disk = self.suite.path + '.disk' + if persist != 'noerase': + try: + with open(disk, 'w') as f: + f.truncate(0) + if args.get('verbose', False): + print('truncate --size=0', disk) + except FileNotFoundError: + pass + + cmd.append(disk) + + # simulate power-loss after n cycles? + if cycles: + cmd.append(str(cycles)) + + # failed? drop into debugger? + if gdb and failure: + ncmd = ['gdb'] + if gdb == 'assert': + ncmd.extend(['-ex', 'r']) + if failure.assert_: + ncmd.extend(['-ex', 'up 2']) + elif gdb == 'main': + ncmd.extend([ + '-ex', 'b %s:%d' % (self.suite.path, self.code_lineno), + '-ex', 'r']) + ncmd.extend(['--args'] + cmd) + + if args.get('verbose', False): + print(' '.join(shlex.quote(c) for c in ncmd)) + signal.signal(signal.SIGINT, signal.SIG_IGN) + sys.exit(sp.call(ncmd)) + + # run test case! + mpty, spty = pty.openpty() + if args.get('verbose', False): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, stdout=spty, stderr=spty) + os.close(spty) + mpty = os.fdopen(mpty, 'r', 1) + stdout = [] + assert_ = None + try: + while True: + try: + line = mpty.readline() + except OSError as e: + if e.errno == errno.EIO: + break + raise + stdout.append(line) + if args.get('verbose', False): + sys.stdout.write(line) + # intercept asserts + m = re.match( + '^{0}([^:]+):(\d+):(?:\d+:)?{0}{1}:{0}(.*)$' + .format('(?:\033\[[\d;]*.| )*', 'assert'), + line) + if m and assert_ is None: + try: + with open(m.group(1)) as f: + lineno = int(m.group(2)) + line = (next(it.islice(f, lineno-1, None)) + .strip('\n')) + assert_ = { + 'path': m.group(1), + 'line': line, + 'lineno': lineno, + 'message': m.group(3)} + except: + pass + except KeyboardInterrupt: + raise TestFailure(self, 1, stdout, None) + proc.wait() + + # did we pass? + if proc.returncode != 0: + raise TestFailure(self, proc.returncode, stdout, assert_) + else: + return PASS + +class ValgrindTestCase(TestCase): + def __init__(self, config, **args): + self.leaky = config.get('leaky', False) + super().__init__(config, **args) + + def shouldtest(self, **args): + return not self.leaky and super().shouldtest(**args) + + def test(self, exec=[], **args): + verbose = args.get('verbose', False) + uninit = (self.defines.get('LFS2_ERASE_VALUE', None) == -1) + exec = [ + 'valgrind', + '--leak-check=full', + ] + (['--undef-value-errors=no'] if uninit else []) + [ + ] + (['--track-origins=yes'] if not uninit else []) + [ + '--error-exitcode=4', + '--error-limit=no', + ] + (['--num-callers=1'] if not verbose else []) + [ + '-q'] + exec + return super().test(exec=exec, **args) + +class ReentrantTestCase(TestCase): + def __init__(self, config, **args): + self.reentrant = config.get('reentrant', False) + super().__init__(config, **args) + + def shouldtest(self, **args): + return self.reentrant and super().shouldtest(**args) + + def test(self, persist=False, gdb=False, failure=None, **args): + for cycles in it.count(1): + # clear disk first? + if cycles == 1 and persist != 'noerase': + persist = 'erase' + else: + persist = 'noerase' + + # exact cycle we should drop into debugger? + if gdb and failure and failure.cycleno == cycles: + return super().test(gdb=gdb, persist=persist, cycles=cycles, + failure=failure, **args) + + # run tests, but kill the program after prog/erase has + # been hit n cycles. We exit with a special return code if the + # program has not finished, since this isn't a test failure. + try: + return super().test(persist=persist, cycles=cycles, **args) + except TestFailure as nfailure: + if nfailure.returncode == 33: + continue + else: + nfailure.cycleno = cycles + raise + +class TestSuite: + def __init__(self, path, classes=[TestCase], defines={}, + filter=None, **args): + self.name = os.path.basename(path) + if self.name.endswith('.toml'): + self.name = self.name[:-len('.toml')] + self.path = path + self.classes = classes + self.defines = defines.copy() + self.filter = filter + + with open(path) as f: + # load tests + config = toml.load(f) + + # find line numbers + f.seek(0) + linenos = [] + code_linenos = [] + for i, line in enumerate(f): + if re.match(r'\[\[\s*case\s*\]\]', line): + linenos.append(i+1) + if re.match(r'code\s*=\s*(\'\'\'|""")', line): + code_linenos.append(i+2) + + code_linenos.reverse() + + # grab global config + for k, v in config.get('define', {}).items(): + if k not in self.defines: + self.defines[k] = v + self.code = config.get('code', None) + if self.code is not None: + self.code_lineno = code_linenos.pop() + + # create initial test cases + self.cases = [] + for i, (case, lineno) in enumerate(zip(config['case'], linenos)): + # code lineno? + if 'code' in case: + case['code_lineno'] = code_linenos.pop() + # merge conditions if necessary + if 'if' in config and 'if' in case: + case['if'] = '(%s) && (%s)' % (config['if'], case['if']) + elif 'if' in config: + case['if'] = config['if'] + # initialize test case + self.cases.append(TestCase(case, filter=filter, + suite=self, caseno=i+1, lineno=lineno, **args)) + + def __str__(self): + return self.name + + def __lt__(self, other): + return self.name < other.name + + def permute(self, **args): + for case in self.cases: + # lets find all parameterized definitions, in one of [args.D, + # suite.defines, case.defines, DEFINES]. Note that each of these + # can be either a dict of defines, or a list of dicts, expressing + # an initial set of permutations. + pending = [{}] + for inits in [self.defines, case.defines, DEFINES]: + if not isinstance(inits, list): + inits = [inits] + + npending = [] + for init, pinit in it.product(inits, pending): + ninit = pinit.copy() + for k, v in init.items(): + if k not in ninit: + try: + ninit[k] = eval(v) + except: + ninit[k] = v + npending.append(ninit) + + pending = npending + + # expand permutations + pending = list(reversed(pending)) + expanded = [] + while pending: + perm = pending.pop() + for k, v in sorted(perm.items()): + if not isinstance(v, str) and isinstance(v, abc.Iterable): + for nv in reversed(v): + nperm = perm.copy() + nperm[k] = nv + pending.append(nperm) + break + else: + expanded.append(perm) + + # generate permutations + case.perms = [] + for i, (class_, defines) in enumerate( + it.product(self.classes, expanded)): + case.perms.append(case.permute( + class_, defines, permno=i+1, **args)) + + # also track non-unique defines + case.defines = {} + for k, v in case.perms[0].defines.items(): + if all(perm.defines[k] == v for perm in case.perms): + case.defines[k] = v + + # track all perms and non-unique defines + self.perms = [] + for case in self.cases: + self.perms.extend(case.perms) + + self.defines = {} + for k, v in self.perms[0].defines.items(): + if all(perm.defines.get(k, None) == v for perm in self.perms): + self.defines[k] = v + + return self.perms + + def build(self, **args): + # build test files + tf = open(self.path + '.test.c.t', 'w') + tf.write(GLOBALS) + if self.code is not None: + tf.write('#line %d "%s"\n' % (self.code_lineno, self.path)) + tf.write(self.code) + + tfs = {None: tf} + for case in self.cases: + if case.in_ not in tfs: + tfs[case.in_] = open(self.path+'.'+ + case.in_.replace('/', '.')+'.t', 'w') + tfs[case.in_].write('#line 1 "%s"\n' % case.in_) + with open(case.in_) as f: + for line in f: + tfs[case.in_].write(line) + tfs[case.in_].write('\n') + tfs[case.in_].write(GLOBALS) + + tfs[case.in_].write('\n') + case.build(tfs[case.in_], **args) + + tf.write('\n') + tf.write('const char *lfs2_testbd_path;\n') + tf.write('uint32_t lfs2_testbd_cycles;\n') + tf.write('int main(int argc, char **argv) {\n') + tf.write(4*' '+'int case_ = (argc > 1) ? atoi(argv[1]) : 0;\n') + tf.write(4*' '+'int perm = (argc > 2) ? atoi(argv[2]) : 0;\n') + tf.write(4*' '+'lfs2_testbd_path = (argc > 3) ? argv[3] : NULL;\n') + tf.write(4*' '+'lfs2_testbd_cycles = (argc > 4) ? atoi(argv[4]) : 0;\n') + for perm in self.perms: + # test declaration + tf.write(4*' '+'extern void test_case%d(%s);\n' % ( + perm.caseno, ', '.join( + 'intmax_t %s' % k for k in sorted(perm.defines) + if k not in perm.case.defines))) + # test call + tf.write(4*' '+ + 'if (argc < 3 || (case_ == %d && perm == %d)) {' + ' test_case%d(%s); ' + '}\n' % (perm.caseno, perm.permno, perm.caseno, ', '.join( + str(v) for k, v in sorted(perm.defines.items()) + if k not in perm.case.defines))) + tf.write('}\n') + + for tf in tfs.values(): + tf.close() + + # write makefiles + with open(self.path + '.mk', 'w') as mk: + mk.write(RULES.replace(4*' ', '\t')) + mk.write('\n') + + # add truely global defines globally + for k, v in sorted(self.defines.items()): + mk.write('%s: override CFLAGS += -D%s=%r\n' % ( + self.path+'.test', k, v)) + + for path in tfs: + if path is None: + mk.write('%s: %s | %s\n' % ( + self.path+'.test.c', + self.path, + self.path+'.test.c.t')) + else: + mk.write('%s: %s %s | %s\n' % ( + self.path+'.'+path.replace('/', '.'), + self.path, path, + self.path+'.'+path.replace('/', '.')+'.t')) + mk.write('\t./scripts/explode_asserts.py $| -o $@\n') + + self.makefile = self.path + '.mk' + self.target = self.path + '.test' + return self.makefile, self.target + + def test(self, **args): + # run test suite! + if not args.get('verbose', True): + sys.stdout.write(self.name + ' ') + sys.stdout.flush() + for perm in self.perms: + if not perm.shouldtest(**args): + continue + + try: + result = perm.test(**args) + except TestFailure as failure: + perm.result = failure + if not args.get('verbose', True): + sys.stdout.write(FAIL) + sys.stdout.flush() + if not args.get('keep_going', False): + if not args.get('verbose', True): + sys.stdout.write('\n') + raise + else: + perm.result = PASS + if not args.get('verbose', True): + sys.stdout.write(PASS) + sys.stdout.flush() + + if not args.get('verbose', True): + sys.stdout.write('\n') + +def main(**args): + # figure out explicit defines + defines = {} + for define in args['D']: + k, v, *_ = define.split('=', 2) + [''] + defines[k] = v + + # and what class of TestCase to run + classes = [] + if args.get('normal', False): + classes.append(TestCase) + if args.get('reentrant', False): + classes.append(ReentrantTestCase) + if args.get('valgrind', False): + classes.append(ValgrindTestCase) + if not classes: + classes = [TestCase] + + suites = [] + for testpath in args['testpaths']: + # optionally specified test case/perm + testpath, *filter = testpath.split('#') + filter = [int(f) for f in filter] + + # figure out the suite's toml file + if os.path.isdir(testpath): + testpath = testpath + '/test_*.toml' + elif os.path.isfile(testpath): + testpath = testpath + elif testpath.endswith('.toml'): + testpath = TESTDIR + '/' + testpath + else: + testpath = TESTDIR + '/' + testpath + '.toml' + + # find tests + for path in glob.glob(testpath): + suites.append(TestSuite(path, classes, defines, filter, **args)) + + # sort for reproducability + suites = sorted(suites) + + # generate permutations + for suite in suites: + suite.permute(**args) + + # build tests in parallel + print('====== building ======') + makefiles = [] + targets = [] + for suite in suites: + makefile, target = suite.build(**args) + makefiles.append(makefile) + targets.append(target) + + cmd = (['make', '-f', 'Makefile'] + + list(it.chain.from_iterable(['-f', m] for m in makefiles)) + + [target for target in targets]) + mpty, spty = pty.openpty() + if args.get('verbose', False): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, stdout=spty, stderr=spty) + os.close(spty) + mpty = os.fdopen(mpty, 'r', 1) + stdout = [] + while True: + try: + line = mpty.readline() + except OSError as e: + if e.errno == errno.EIO: + break + raise + stdout.append(line) + if args.get('verbose', False): + sys.stdout.write(line) + # intercept warnings + m = re.match( + '^{0}([^:]+):(\d+):(?:\d+:)?{0}{1}:{0}(.*)$' + .format('(?:\033\[[\d;]*.| )*', 'warning'), + line) + if m and not args.get('verbose', False): + try: + with open(m.group(1)) as f: + lineno = int(m.group(2)) + line = next(it.islice(f, lineno-1, None)).strip('\n') + sys.stdout.write( + "\033[01m{path}:{lineno}:\033[01;35mwarning:\033[m " + "{message}\n{line}\n\n".format( + path=m.group(1), line=line, lineno=lineno, + message=m.group(3))) + except: + pass + proc.wait() + + if proc.returncode != 0: + if not args.get('verbose', False): + for line in stdout: + sys.stdout.write(line) + sys.exit(-3) + + print('built %d test suites, %d test cases, %d permutations' % ( + len(suites), + sum(len(suite.cases) for suite in suites), + sum(len(suite.perms) for suite in suites))) + + filtered = 0 + for suite in suites: + for perm in suite.perms: + filtered += perm.shouldtest(**args) + if filtered != sum(len(suite.perms) for suite in suites): + print('filtered down to %d permutations' % filtered) + + # only requested to build? + if args.get('build', False): + return 0 + + print('====== testing ======') + try: + for suite in suites: + suite.test(**args) + except TestFailure: + pass + + print('====== results ======') + passed = 0 + failed = 0 + for suite in suites: + for perm in suite.perms: + if not hasattr(perm, 'result'): + continue + + if perm.result == PASS: + passed += 1 + else: + sys.stdout.write( + "\033[01m{path}:{lineno}:\033[01;31mfailure:\033[m " + "{perm} failed with {returncode}\n".format( + perm=perm, path=perm.suite.path, lineno=perm.lineno, + returncode=perm.result.returncode or 0)) + if perm.result.stdout: + if perm.result.assert_: + stdout = perm.result.stdout[:-1] + else: + stdout = perm.result.stdout + for line in stdout[-5:]: + sys.stdout.write(line) + if perm.result.assert_: + sys.stdout.write( + "\033[01m{path}:{lineno}:\033[01;31massert:\033[m " + "{message}\n{line}\n".format( + **perm.result.assert_)) + sys.stdout.write('\n') + failed += 1 + + if args.get('gdb', False): + failure = None + for suite in suites: + for perm in suite.perms: + if getattr(perm, 'result', PASS) != PASS: + failure = perm.result + if failure is not None: + print('======= gdb ======') + # drop into gdb + failure.case.test(failure=failure, **args) + sys.exit(0) + + print('tests passed: %d' % passed) + print('tests failed: %d' % failed) + return 1 if failed > 0 else 0 + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser( + description="Run parameterized tests in various configurations.") + parser.add_argument('testpaths', nargs='*', default=[TESTDIR], + help="Description of test(s) to run. By default, this is all tests \ + found in the \"{0}\" directory. Here, you can specify a different \ + directory of tests, a specific file, a suite by name, and even a \ + specific test case by adding brackets. For example \ + \"test_dirs[0]\" or \"{0}/test_dirs.toml[0]\".".format(TESTDIR)) + parser.add_argument('-D', action='append', default=[], + help="Overriding parameter definitions.") + parser.add_argument('-v', '--verbose', action='store_true', + help="Output everything that is happening.") + parser.add_argument('-k', '--keep-going', action='store_true', + help="Run all tests instead of stopping on first error. Useful for CI.") + parser.add_argument('-p', '--persist', choices=['erase', 'noerase'], + nargs='?', const='erase', + help="Store disk image in a file.") + parser.add_argument('-b', '--build', action='store_true', + help="Only build the tests, do not execute.") + parser.add_argument('-g', '--gdb', choices=['init', 'main', 'assert'], + nargs='?', const='assert', + help="Drop into gdb on test failure.") + parser.add_argument('--no-internal', action='store_true', + help="Don't run tests that require internal knowledge.") + parser.add_argument('-n', '--normal', action='store_true', + help="Run tests normally.") + parser.add_argument('-r', '--reentrant', action='store_true', + help="Run reentrant tests with simulated power-loss.") + parser.add_argument('-V', '--valgrind', action='store_true', + help="Run non-leaky tests under valgrind to check for memory leaks.") + parser.add_argument('-e', '--exec', default=[], type=lambda e: e.split(' '), + help="Run tests with another executable prefixed on the command line.") + parser.add_argument('-d', '--disk', + help="Specify a file to use for persistent/reentrant tests.") + sys.exit(main(**vars(parser.parse_args()))) diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_alloc.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_alloc.toml new file mode 100644 index 00000000000..b547b254e50 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_alloc.toml @@ -0,0 +1,653 @@ +# allocator tests +# note for these to work there are a number constraints on the device geometry +if = 'LFS2_BLOCK_CYCLES == -1' + +[[case]] # parallel allocation test +define.FILES = 3 +define.SIZE = '(((LFS2_BLOCK_SIZE-8)*(LFS2_BLOCK_COUNT-6)) / FILES)' +code = ''' + const char *names[FILES] = {"bacon", "eggs", "pancakes"}; + lfs2_file_t files[FILES]; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "breakfast") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int n = 0; n < FILES; n++) { + sprintf(path, "breakfast/%s", names[n]); + lfs2_file_open(&lfs2, &files[n], path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + } + for (int n = 0; n < FILES; n++) { + size = strlen(names[n]); + for (lfs2_size_t i = 0; i < SIZE; i += size) { + lfs2_file_write(&lfs2, &files[n], names[n], size) => size; + } + } + for (int n = 0; n < FILES; n++) { + lfs2_file_close(&lfs2, &files[n]) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int n = 0; n < FILES; n++) { + sprintf(path, "breakfast/%s", names[n]); + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + size = strlen(names[n]); + for (lfs2_size_t i = 0; i < SIZE; i += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(memcmp(buffer, names[n], size) == 0); + } + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # serial allocation test +define.FILES = 3 +define.SIZE = '(((LFS2_BLOCK_SIZE-8)*(LFS2_BLOCK_COUNT-6)) / FILES)' +code = ''' + const char *names[FILES] = {"bacon", "eggs", "pancakes"}; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "breakfast") => 0; + lfs2_unmount(&lfs2) => 0; + + for (int n = 0; n < FILES; n++) { + lfs2_mount(&lfs2, &cfg) => 0; + sprintf(path, "breakfast/%s", names[n]); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + size = strlen(names[n]); + memcpy(buffer, names[n], size); + for (int i = 0; i < SIZE; i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + } + + lfs2_mount(&lfs2, &cfg) => 0; + for (int n = 0; n < FILES; n++) { + sprintf(path, "breakfast/%s", names[n]); + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + size = strlen(names[n]); + for (int i = 0; i < SIZE; i += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(memcmp(buffer, names[n], size) == 0); + } + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # parallel allocation reuse test +define.FILES = 3 +define.SIZE = '(((LFS2_BLOCK_SIZE-8)*(LFS2_BLOCK_COUNT-6)) / FILES)' +define.CYCLES = [1, 10] +code = ''' + const char *names[FILES] = {"bacon", "eggs", "pancakes"}; + lfs2_file_t files[FILES]; + + lfs2_format(&lfs2, &cfg) => 0; + + for (int c = 0; c < CYCLES; c++) { + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "breakfast") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int n = 0; n < FILES; n++) { + sprintf(path, "breakfast/%s", names[n]); + lfs2_file_open(&lfs2, &files[n], path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + } + for (int n = 0; n < FILES; n++) { + size = strlen(names[n]); + for (int i = 0; i < SIZE; i += size) { + lfs2_file_write(&lfs2, &files[n], names[n], size) => size; + } + } + for (int n = 0; n < FILES; n++) { + lfs2_file_close(&lfs2, &files[n]) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int n = 0; n < FILES; n++) { + sprintf(path, "breakfast/%s", names[n]); + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + size = strlen(names[n]); + for (int i = 0; i < SIZE; i += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(memcmp(buffer, names[n], size) == 0); + } + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int n = 0; n < FILES; n++) { + sprintf(path, "breakfast/%s", names[n]); + lfs2_remove(&lfs2, path) => 0; + } + lfs2_remove(&lfs2, "breakfast") => 0; + lfs2_unmount(&lfs2) => 0; + } +''' + +[[case]] # serial allocation reuse test +define.FILES = 3 +define.SIZE = '(((LFS2_BLOCK_SIZE-8)*(LFS2_BLOCK_COUNT-6)) / FILES)' +define.CYCLES = [1, 10] +code = ''' + const char *names[FILES] = {"bacon", "eggs", "pancakes"}; + + lfs2_format(&lfs2, &cfg) => 0; + + for (int c = 0; c < CYCLES; c++) { + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "breakfast") => 0; + lfs2_unmount(&lfs2) => 0; + + for (int n = 0; n < FILES; n++) { + lfs2_mount(&lfs2, &cfg) => 0; + sprintf(path, "breakfast/%s", names[n]); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + size = strlen(names[n]); + memcpy(buffer, names[n], size); + for (int i = 0; i < SIZE; i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + } + + lfs2_mount(&lfs2, &cfg) => 0; + for (int n = 0; n < FILES; n++) { + sprintf(path, "breakfast/%s", names[n]); + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + size = strlen(names[n]); + for (int i = 0; i < SIZE; i += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(memcmp(buffer, names[n], size) == 0); + } + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int n = 0; n < FILES; n++) { + sprintf(path, "breakfast/%s", names[n]); + lfs2_remove(&lfs2, path) => 0; + } + lfs2_remove(&lfs2, "breakfast") => 0; + lfs2_unmount(&lfs2) => 0; + } +''' + +[[case]] # exhaustion test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_WRONLY | LFS2_O_CREAT); + size = strlen("exhaustion"); + memcpy(buffer, "exhaustion", size); + lfs2_file_write(&lfs2, &file, buffer, size) => size; + lfs2_file_sync(&lfs2, &file) => 0; + + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + lfs2_ssize_t res; + while (true) { + res = lfs2_file_write(&lfs2, &file, buffer, size); + if (res < 0) { + break; + } + + res => size; + } + res => LFS2_ERR_NOSPC; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_RDONLY); + size = strlen("exhaustion"); + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "exhaustion", size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # exhaustion wraparound test +define.SIZE = '(((LFS2_BLOCK_SIZE-8)*(LFS2_BLOCK_COUNT-4)) / 3)' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + lfs2_file_open(&lfs2, &file, "padding", LFS2_O_WRONLY | LFS2_O_CREAT); + size = strlen("buffering"); + memcpy(buffer, "buffering", size); + for (int i = 0; i < SIZE; i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_remove(&lfs2, "padding") => 0; + + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_WRONLY | LFS2_O_CREAT); + size = strlen("exhaustion"); + memcpy(buffer, "exhaustion", size); + lfs2_file_write(&lfs2, &file, buffer, size) => size; + lfs2_file_sync(&lfs2, &file) => 0; + + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + lfs2_ssize_t res; + while (true) { + res = lfs2_file_write(&lfs2, &file, buffer, size); + if (res < 0) { + break; + } + + res => size; + } + res => LFS2_ERR_NOSPC; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_RDONLY); + size = strlen("exhaustion"); + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "exhaustion", size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_remove(&lfs2, "exhaustion") => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # dir exhaustion test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // find out max file size + lfs2_mkdir(&lfs2, "exhaustiondir") => 0; + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_WRONLY | LFS2_O_CREAT); + int count = 0; + while (true) { + err = lfs2_file_write(&lfs2, &file, buffer, size); + if (err < 0) { + break; + } + + count += 1; + } + err => LFS2_ERR_NOSPC; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_remove(&lfs2, "exhaustion") => 0; + lfs2_remove(&lfs2, "exhaustiondir") => 0; + + // see if dir fits with max file size + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_WRONLY | LFS2_O_CREAT); + for (int i = 0; i < count; i++) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_mkdir(&lfs2, "exhaustiondir") => 0; + lfs2_remove(&lfs2, "exhaustiondir") => 0; + lfs2_remove(&lfs2, "exhaustion") => 0; + + // see if dir fits with > max file size + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_WRONLY | LFS2_O_CREAT); + for (int i = 0; i < count+1; i++) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_mkdir(&lfs2, "exhaustiondir") => LFS2_ERR_NOSPC; + + lfs2_remove(&lfs2, "exhaustion") => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # what if we have a bad block during an allocation scan? +in = "lfs2.c" +define.LFS2_ERASE_CYCLES = 0xffffffff +define.LFS2_BADBLOCK_BEHAVIOR = 'LFS2_TESTBD_BADBLOCK_READERROR' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + // first fill to exhaustion to find available space + lfs2_file_open(&lfs2, &file, "pacman", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + strcpy((char*)buffer, "waka"); + size = strlen("waka"); + lfs2_size_t filesize = 0; + while (true) { + lfs2_ssize_t res = lfs2_file_write(&lfs2, &file, buffer, size); + assert(res == (lfs2_ssize_t)size || res == LFS2_ERR_NOSPC); + if (res == LFS2_ERR_NOSPC) { + break; + } + filesize += size; + } + lfs2_file_close(&lfs2, &file) => 0; + // now fill all but a couple of blocks of the filesystem with data + filesize -= 3*LFS2_BLOCK_SIZE; + lfs2_file_open(&lfs2, &file, "pacman", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + strcpy((char*)buffer, "waka"); + size = strlen("waka"); + for (lfs2_size_t i = 0; i < filesize/size; i++) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + // also save head of file so we can error during lookahead scan + lfs2_block_t fileblock = file.ctz.head; + lfs2_unmount(&lfs2) => 0; + + // remount to force an alloc scan + lfs2_mount(&lfs2, &cfg) => 0; + + // but mark the head of our file as a "bad block", this is force our + // scan to bail early + lfs2_testbd_setwear(&cfg, fileblock, 0xffffffff) => 0; + lfs2_file_open(&lfs2, &file, "ghost", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + strcpy((char*)buffer, "chomp"); + size = strlen("chomp"); + while (true) { + lfs2_ssize_t res = lfs2_file_write(&lfs2, &file, buffer, size); + assert(res == (lfs2_ssize_t)size || res == LFS2_ERR_CORRUPT); + if (res == LFS2_ERR_CORRUPT) { + break; + } + } + lfs2_file_close(&lfs2, &file) => 0; + + // now reverse the "bad block" and try to write the file again until we + // run out of space + lfs2_testbd_setwear(&cfg, fileblock, 0) => 0; + lfs2_file_open(&lfs2, &file, "ghost", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + strcpy((char*)buffer, "chomp"); + size = strlen("chomp"); + while (true) { + lfs2_ssize_t res = lfs2_file_write(&lfs2, &file, buffer, size); + assert(res == (lfs2_ssize_t)size || res == LFS2_ERR_NOSPC); + if (res == LFS2_ERR_NOSPC) { + break; + } + } + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; + + // check that the disk isn't hurt + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "pacman", LFS2_O_RDONLY) => 0; + strcpy((char*)buffer, "waka"); + size = strlen("waka"); + for (lfs2_size_t i = 0; i < filesize/size; i++) { + uint8_t rbuffer[4]; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + assert(memcmp(rbuffer, buffer, size) == 0); + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + + +# Below, I don't like these tests. They're fragile and depend _heavily_ +# on the geometry of the block device. But they are valuable. Eventually they +# should be removed and replaced with generalized tests. + +[[case]] # chained dir exhaustion test +define.LFS2_BLOCK_SIZE = 512 +define.LFS2_BLOCK_COUNT = 1024 +if = 'LFS2_BLOCK_SIZE == 512 && LFS2_BLOCK_COUNT == 1024' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // find out max file size + lfs2_mkdir(&lfs2, "exhaustiondir") => 0; + for (int i = 0; i < 10; i++) { + sprintf(path, "dirwithanexhaustivelylongnameforpadding%d", i); + lfs2_mkdir(&lfs2, path) => 0; + } + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_WRONLY | LFS2_O_CREAT); + int count = 0; + while (true) { + err = lfs2_file_write(&lfs2, &file, buffer, size); + if (err < 0) { + break; + } + + count += 1; + } + err => LFS2_ERR_NOSPC; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_remove(&lfs2, "exhaustion") => 0; + lfs2_remove(&lfs2, "exhaustiondir") => 0; + for (int i = 0; i < 10; i++) { + sprintf(path, "dirwithanexhaustivelylongnameforpadding%d", i); + lfs2_remove(&lfs2, path) => 0; + } + + // see that chained dir fails + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_WRONLY | LFS2_O_CREAT); + for (int i = 0; i < count+1; i++) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_sync(&lfs2, &file) => 0; + + for (int i = 0; i < 10; i++) { + sprintf(path, "dirwithanexhaustivelylongnameforpadding%d", i); + lfs2_mkdir(&lfs2, path) => 0; + } + + lfs2_mkdir(&lfs2, "exhaustiondir") => LFS2_ERR_NOSPC; + + // shorten file to try a second chained dir + while (true) { + err = lfs2_mkdir(&lfs2, "exhaustiondir"); + if (err != LFS2_ERR_NOSPC) { + break; + } + + lfs2_ssize_t filesize = lfs2_file_size(&lfs2, &file); + filesize > 0 => true; + + lfs2_file_truncate(&lfs2, &file, filesize - size) => 0; + lfs2_file_sync(&lfs2, &file) => 0; + } + err => 0; + + lfs2_mkdir(&lfs2, "exhaustiondir2") => LFS2_ERR_NOSPC; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # split dir test +define.LFS2_BLOCK_SIZE = 512 +define.LFS2_BLOCK_COUNT = 1024 +if = 'LFS2_BLOCK_SIZE == 512 && LFS2_BLOCK_COUNT == 1024' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // create one block hole for half a directory + lfs2_file_open(&lfs2, &file, "bump", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + for (lfs2_size_t i = 0; i < cfg.block_size; i += 2) { + memcpy(&buffer[i], "hi", 2); + } + lfs2_file_write(&lfs2, &file, buffer, cfg.block_size) => cfg.block_size; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "exhaustion", LFS2_O_WRONLY | LFS2_O_CREAT); + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + for (lfs2_size_t i = 0; + i < (cfg.block_count-4)*(cfg.block_size-8); + i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + // remount to force reset of lookahead + lfs2_unmount(&lfs2) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // open hole + lfs2_remove(&lfs2, "bump") => 0; + + lfs2_mkdir(&lfs2, "splitdir") => 0; + lfs2_file_open(&lfs2, &file, "splitdir/bump", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + for (lfs2_size_t i = 0; i < cfg.block_size; i += 2) { + memcpy(&buffer[i], "hi", 2); + } + lfs2_file_write(&lfs2, &file, buffer, 2*cfg.block_size) => LFS2_ERR_NOSPC; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # outdated lookahead test +define.LFS2_BLOCK_SIZE = 512 +define.LFS2_BLOCK_COUNT = 1024 +if = 'LFS2_BLOCK_SIZE == 512 && LFS2_BLOCK_COUNT == 1024' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // fill completely with two files + lfs2_file_open(&lfs2, &file, "exhaustion1", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + for (lfs2_size_t i = 0; + i < ((cfg.block_count-2)/2)*(cfg.block_size-8); + i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "exhaustion2", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + for (lfs2_size_t i = 0; + i < ((cfg.block_count-2+1)/2)*(cfg.block_size-8); + i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + // remount to force reset of lookahead + lfs2_unmount(&lfs2) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // rewrite one file + lfs2_file_open(&lfs2, &file, "exhaustion1", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_sync(&lfs2, &file) => 0; + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + for (lfs2_size_t i = 0; + i < ((cfg.block_count-2)/2)*(cfg.block_size-8); + i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + // rewrite second file, this requires lookahead does not + // use old population + lfs2_file_open(&lfs2, &file, "exhaustion2", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_sync(&lfs2, &file) => 0; + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + for (lfs2_size_t i = 0; + i < ((cfg.block_count-2+1)/2)*(cfg.block_size-8); + i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # outdated lookahead and split dir test +define.LFS2_BLOCK_SIZE = 512 +define.LFS2_BLOCK_COUNT = 1024 +if = 'LFS2_BLOCK_SIZE == 512 && LFS2_BLOCK_COUNT == 1024' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // fill completely with two files + lfs2_file_open(&lfs2, &file, "exhaustion1", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + for (lfs2_size_t i = 0; + i < ((cfg.block_count-2)/2)*(cfg.block_size-8); + i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "exhaustion2", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + for (lfs2_size_t i = 0; + i < ((cfg.block_count-2+1)/2)*(cfg.block_size-8); + i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + // remount to force reset of lookahead + lfs2_unmount(&lfs2) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // rewrite one file with a hole of one block + lfs2_file_open(&lfs2, &file, "exhaustion1", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_sync(&lfs2, &file) => 0; + size = strlen("blahblahblahblah"); + memcpy(buffer, "blahblahblahblah", size); + for (lfs2_size_t i = 0; + i < ((cfg.block_count-2)/2 - 1)*(cfg.block_size-8); + i += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + // try to allocate a directory, should fail! + lfs2_mkdir(&lfs2, "split") => LFS2_ERR_NOSPC; + + // file should not fail + lfs2_file_open(&lfs2, &file, "notasplit", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "hi", 2) => 2; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_attrs.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_attrs.toml new file mode 100644 index 00000000000..274cdfe668a --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_attrs.toml @@ -0,0 +1,304 @@ +[[case]] # set/get attribute +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "hello") => 0; + lfs2_file_open(&lfs2, &file, "hello/hello", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "hello", strlen("hello")) => strlen("hello"); + lfs2_file_close(&lfs2, &file); + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + memset(buffer, 0, sizeof(buffer)); + lfs2_setattr(&lfs2, "hello", 'A', "aaaa", 4) => 0; + lfs2_setattr(&lfs2, "hello", 'B', "bbbbbb", 6) => 0; + lfs2_setattr(&lfs2, "hello", 'C', "ccccc", 5) => 0; + lfs2_getattr(&lfs2, "hello", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "hello", 'B', buffer+4, 6) => 6; + lfs2_getattr(&lfs2, "hello", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "bbbbbb", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_setattr(&lfs2, "hello", 'B', "", 0) => 0; + lfs2_getattr(&lfs2, "hello", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "hello", 'B', buffer+4, 6) => 0; + lfs2_getattr(&lfs2, "hello", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "\0\0\0\0\0\0", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_removeattr(&lfs2, "hello", 'B') => 0; + lfs2_getattr(&lfs2, "hello", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "hello", 'B', buffer+4, 6) => LFS2_ERR_NOATTR; + lfs2_getattr(&lfs2, "hello", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "\0\0\0\0\0\0", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_setattr(&lfs2, "hello", 'B', "dddddd", 6) => 0; + lfs2_getattr(&lfs2, "hello", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "hello", 'B', buffer+4, 6) => 6; + lfs2_getattr(&lfs2, "hello", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "dddddd", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_setattr(&lfs2, "hello", 'B', "eee", 3) => 0; + lfs2_getattr(&lfs2, "hello", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "hello", 'B', buffer+4, 6) => 3; + lfs2_getattr(&lfs2, "hello", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "eee\0\0\0", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_setattr(&lfs2, "hello", 'A', buffer, LFS2_ATTR_MAX+1) => LFS2_ERR_NOSPC; + lfs2_setattr(&lfs2, "hello", 'B', "fffffffff", 9) => 0; + lfs2_getattr(&lfs2, "hello", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "hello", 'B', buffer+4, 6) => 9; + lfs2_getattr(&lfs2, "hello", 'C', buffer+10, 5) => 5; + + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + memset(buffer, 0, sizeof(buffer)); + lfs2_getattr(&lfs2, "hello", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "hello", 'B', buffer+4, 9) => 9; + lfs2_getattr(&lfs2, "hello", 'C', buffer+13, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "fffffffff", 9) => 0; + memcmp(buffer+13, "ccccc", 5) => 0; + + lfs2_file_open(&lfs2, &file, "hello/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, sizeof(buffer)) => strlen("hello"); + memcmp(buffer, "hello", strlen("hello")) => 0; + lfs2_file_close(&lfs2, &file); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # set/get root attribute +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "hello") => 0; + lfs2_file_open(&lfs2, &file, "hello/hello", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "hello", strlen("hello")) => strlen("hello"); + lfs2_file_close(&lfs2, &file); + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + memset(buffer, 0, sizeof(buffer)); + lfs2_setattr(&lfs2, "/", 'A', "aaaa", 4) => 0; + lfs2_setattr(&lfs2, "/", 'B', "bbbbbb", 6) => 0; + lfs2_setattr(&lfs2, "/", 'C', "ccccc", 5) => 0; + lfs2_getattr(&lfs2, "/", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "/", 'B', buffer+4, 6) => 6; + lfs2_getattr(&lfs2, "/", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "bbbbbb", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_setattr(&lfs2, "/", 'B', "", 0) => 0; + lfs2_getattr(&lfs2, "/", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "/", 'B', buffer+4, 6) => 0; + lfs2_getattr(&lfs2, "/", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "\0\0\0\0\0\0", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_removeattr(&lfs2, "/", 'B') => 0; + lfs2_getattr(&lfs2, "/", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "/", 'B', buffer+4, 6) => LFS2_ERR_NOATTR; + lfs2_getattr(&lfs2, "/", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "\0\0\0\0\0\0", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_setattr(&lfs2, "/", 'B', "dddddd", 6) => 0; + lfs2_getattr(&lfs2, "/", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "/", 'B', buffer+4, 6) => 6; + lfs2_getattr(&lfs2, "/", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "dddddd", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_setattr(&lfs2, "/", 'B', "eee", 3) => 0; + lfs2_getattr(&lfs2, "/", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "/", 'B', buffer+4, 6) => 3; + lfs2_getattr(&lfs2, "/", 'C', buffer+10, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "eee\0\0\0", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + lfs2_setattr(&lfs2, "/", 'A', buffer, LFS2_ATTR_MAX+1) => LFS2_ERR_NOSPC; + lfs2_setattr(&lfs2, "/", 'B', "fffffffff", 9) => 0; + lfs2_getattr(&lfs2, "/", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "/", 'B', buffer+4, 6) => 9; + lfs2_getattr(&lfs2, "/", 'C', buffer+10, 5) => 5; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + memset(buffer, 0, sizeof(buffer)); + lfs2_getattr(&lfs2, "/", 'A', buffer, 4) => 4; + lfs2_getattr(&lfs2, "/", 'B', buffer+4, 9) => 9; + lfs2_getattr(&lfs2, "/", 'C', buffer+13, 5) => 5; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "fffffffff", 9) => 0; + memcmp(buffer+13, "ccccc", 5) => 0; + + lfs2_file_open(&lfs2, &file, "hello/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, sizeof(buffer)) => strlen("hello"); + memcmp(buffer, "hello", strlen("hello")) => 0; + lfs2_file_close(&lfs2, &file); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # set/get file attribute +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "hello") => 0; + lfs2_file_open(&lfs2, &file, "hello/hello", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "hello", strlen("hello")) => strlen("hello"); + lfs2_file_close(&lfs2, &file); + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + memset(buffer, 0, sizeof(buffer)); + struct lfs2_attr attrs1[] = { + {'A', buffer, 4}, + {'B', buffer+4, 6}, + {'C', buffer+10, 5}, + }; + struct lfs2_file_config cfg1 = {.attrs=attrs1, .attr_count=3}; + + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_WRONLY, &cfg1) => 0; + memcpy(buffer, "aaaa", 4); + memcpy(buffer+4, "bbbbbb", 6); + memcpy(buffer+10, "ccccc", 5); + lfs2_file_close(&lfs2, &file) => 0; + memset(buffer, 0, 15); + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_RDONLY, &cfg1) => 0; + lfs2_file_close(&lfs2, &file) => 0; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "bbbbbb", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + attrs1[1].size = 0; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_WRONLY, &cfg1) => 0; + lfs2_file_close(&lfs2, &file) => 0; + memset(buffer, 0, 15); + attrs1[1].size = 6; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_RDONLY, &cfg1) => 0; + lfs2_file_close(&lfs2, &file) => 0; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "\0\0\0\0\0\0", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + attrs1[1].size = 6; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_WRONLY, &cfg1) => 0; + memcpy(buffer+4, "dddddd", 6); + lfs2_file_close(&lfs2, &file) => 0; + memset(buffer, 0, 15); + attrs1[1].size = 6; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_RDONLY, &cfg1) => 0; + lfs2_file_close(&lfs2, &file) => 0; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "dddddd", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + attrs1[1].size = 3; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_WRONLY, &cfg1) => 0; + memcpy(buffer+4, "eee", 3); + lfs2_file_close(&lfs2, &file) => 0; + memset(buffer, 0, 15); + attrs1[1].size = 6; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_RDONLY, &cfg1) => 0; + lfs2_file_close(&lfs2, &file) => 0; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "eee\0\0\0", 6) => 0; + memcmp(buffer+10, "ccccc", 5) => 0; + + attrs1[0].size = LFS2_ATTR_MAX+1; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_WRONLY, &cfg1) + => LFS2_ERR_NOSPC; + + struct lfs2_attr attrs2[] = { + {'A', buffer, 4}, + {'B', buffer+4, 9}, + {'C', buffer+13, 5}, + }; + struct lfs2_file_config cfg2 = {.attrs=attrs2, .attr_count=3}; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_RDWR, &cfg2) => 0; + memcpy(buffer+4, "fffffffff", 9); + lfs2_file_close(&lfs2, &file) => 0; + attrs1[0].size = 4; + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_RDONLY, &cfg1) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + memset(buffer, 0, sizeof(buffer)); + struct lfs2_attr attrs3[] = { + {'A', buffer, 4}, + {'B', buffer+4, 9}, + {'C', buffer+13, 5}, + }; + struct lfs2_file_config cfg3 = {.attrs=attrs3, .attr_count=3}; + + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_RDONLY, &cfg3) => 0; + lfs2_file_close(&lfs2, &file) => 0; + memcmp(buffer, "aaaa", 4) => 0; + memcmp(buffer+4, "fffffffff", 9) => 0; + memcmp(buffer+13, "ccccc", 5) => 0; + + lfs2_file_open(&lfs2, &file, "hello/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, sizeof(buffer)) => strlen("hello"); + memcmp(buffer, "hello", strlen("hello")) => 0; + lfs2_file_close(&lfs2, &file); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # deferred file attributes +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "hello") => 0; + lfs2_file_open(&lfs2, &file, "hello/hello", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "hello", strlen("hello")) => strlen("hello"); + lfs2_file_close(&lfs2, &file); + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_setattr(&lfs2, "hello/hello", 'B', "fffffffff", 9) => 0; + lfs2_setattr(&lfs2, "hello/hello", 'C', "ccccc", 5) => 0; + + memset(buffer, 0, sizeof(buffer)); + struct lfs2_attr attrs1[] = { + {'B', "gggg", 4}, + {'C', "", 0}, + {'D', "hhhh", 4}, + }; + struct lfs2_file_config cfg1 = {.attrs=attrs1, .attr_count=3}; + + lfs2_file_opencfg(&lfs2, &file, "hello/hello", LFS2_O_WRONLY, &cfg1) => 0; + + lfs2_getattr(&lfs2, "hello/hello", 'B', buffer, 9) => 9; + lfs2_getattr(&lfs2, "hello/hello", 'C', buffer+9, 9) => 5; + lfs2_getattr(&lfs2, "hello/hello", 'D', buffer+18, 9) => LFS2_ERR_NOATTR; + memcmp(buffer, "fffffffff", 9) => 0; + memcmp(buffer+9, "ccccc\0\0\0\0", 9) => 0; + memcmp(buffer+18, "\0\0\0\0\0\0\0\0\0", 9) => 0; + + lfs2_file_sync(&lfs2, &file) => 0; + lfs2_getattr(&lfs2, "hello/hello", 'B', buffer, 9) => 4; + lfs2_getattr(&lfs2, "hello/hello", 'C', buffer+9, 9) => 0; + lfs2_getattr(&lfs2, "hello/hello", 'D', buffer+18, 9) => 4; + memcmp(buffer, "gggg\0\0\0\0\0", 9) => 0; + memcmp(buffer+9, "\0\0\0\0\0\0\0\0\0", 9) => 0; + memcmp(buffer+18, "hhhh\0\0\0\0\0", 9) => 0; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_badblocks.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_badblocks.toml new file mode 100644 index 00000000000..39a8a87da35 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_badblocks.toml @@ -0,0 +1,241 @@ +# bad blocks with block cycles should be tested in test_relocations +if = 'LFS2_BLOCK_CYCLES == -1' + +[[case]] # single bad blocks +define.LFS2_BLOCK_COUNT = 256 # small bd so test runs faster +define.LFS2_ERASE_CYCLES = 0xffffffff +define.LFS2_ERASE_VALUE = [0x00, 0xff, -1] +define.LFS2_BADBLOCK_BEHAVIOR = [ + 'LFS2_TESTBD_BADBLOCK_PROGERROR', + 'LFS2_TESTBD_BADBLOCK_ERASEERROR', + 'LFS2_TESTBD_BADBLOCK_READERROR', + 'LFS2_TESTBD_BADBLOCK_PROGNOOP', + 'LFS2_TESTBD_BADBLOCK_ERASENOOP', +] +define.NAMEMULT = 64 +define.FILEMULT = 1 +code = ''' + for (lfs2_block_t badblock = 2; badblock < LFS2_BLOCK_COUNT; badblock++) { + lfs2_testbd_setwear(&cfg, badblock-1, 0) => 0; + lfs2_testbd_setwear(&cfg, badblock, 0xffffffff) => 0; + + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 1; i < 10; i++) { + for (int j = 0; j < NAMEMULT; j++) { + buffer[j] = '0'+i; + } + buffer[NAMEMULT] = '\0'; + lfs2_mkdir(&lfs2, (char*)buffer) => 0; + + buffer[NAMEMULT] = '/'; + for (int j = 0; j < NAMEMULT; j++) { + buffer[j+NAMEMULT+1] = '0'+i; + } + buffer[2*NAMEMULT+1] = '\0'; + lfs2_file_open(&lfs2, &file, (char*)buffer, + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + + size = NAMEMULT; + for (int j = 0; j < i*FILEMULT; j++) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 1; i < 10; i++) { + for (int j = 0; j < NAMEMULT; j++) { + buffer[j] = '0'+i; + } + buffer[NAMEMULT] = '\0'; + lfs2_stat(&lfs2, (char*)buffer, &info) => 0; + info.type => LFS2_TYPE_DIR; + + buffer[NAMEMULT] = '/'; + for (int j = 0; j < NAMEMULT; j++) { + buffer[j+NAMEMULT+1] = '0'+i; + } + buffer[2*NAMEMULT+1] = '\0'; + lfs2_file_open(&lfs2, &file, (char*)buffer, LFS2_O_RDONLY) => 0; + + size = NAMEMULT; + for (int j = 0; j < i*FILEMULT; j++) { + uint8_t rbuffer[1024]; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(buffer, rbuffer, size) => 0; + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + } +''' + +[[case]] # region corruption (causes cascading failures) +define.LFS2_BLOCK_COUNT = 256 # small bd so test runs faster +define.LFS2_ERASE_CYCLES = 0xffffffff +define.LFS2_ERASE_VALUE = [0x00, 0xff, -1] +define.LFS2_BADBLOCK_BEHAVIOR = [ + 'LFS2_TESTBD_BADBLOCK_PROGERROR', + 'LFS2_TESTBD_BADBLOCK_ERASEERROR', + 'LFS2_TESTBD_BADBLOCK_READERROR', + 'LFS2_TESTBD_BADBLOCK_PROGNOOP', + 'LFS2_TESTBD_BADBLOCK_ERASENOOP', +] +define.NAMEMULT = 64 +define.FILEMULT = 1 +code = ''' + for (lfs2_block_t i = 0; i < (LFS2_BLOCK_COUNT-2)/2; i++) { + lfs2_testbd_setwear(&cfg, i+2, 0xffffffff) => 0; + } + + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 1; i < 10; i++) { + for (int j = 0; j < NAMEMULT; j++) { + buffer[j] = '0'+i; + } + buffer[NAMEMULT] = '\0'; + lfs2_mkdir(&lfs2, (char*)buffer) => 0; + + buffer[NAMEMULT] = '/'; + for (int j = 0; j < NAMEMULT; j++) { + buffer[j+NAMEMULT+1] = '0'+i; + } + buffer[2*NAMEMULT+1] = '\0'; + lfs2_file_open(&lfs2, &file, (char*)buffer, + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + + size = NAMEMULT; + for (int j = 0; j < i*FILEMULT; j++) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 1; i < 10; i++) { + for (int j = 0; j < NAMEMULT; j++) { + buffer[j] = '0'+i; + } + buffer[NAMEMULT] = '\0'; + lfs2_stat(&lfs2, (char*)buffer, &info) => 0; + info.type => LFS2_TYPE_DIR; + + buffer[NAMEMULT] = '/'; + for (int j = 0; j < NAMEMULT; j++) { + buffer[j+NAMEMULT+1] = '0'+i; + } + buffer[2*NAMEMULT+1] = '\0'; + lfs2_file_open(&lfs2, &file, (char*)buffer, LFS2_O_RDONLY) => 0; + + size = NAMEMULT; + for (int j = 0; j < i*FILEMULT; j++) { + uint8_t rbuffer[1024]; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(buffer, rbuffer, size) => 0; + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # alternating corruption (causes cascading failures) +define.LFS2_BLOCK_COUNT = 256 # small bd so test runs faster +define.LFS2_ERASE_CYCLES = 0xffffffff +define.LFS2_ERASE_VALUE = [0x00, 0xff, -1] +define.LFS2_BADBLOCK_BEHAVIOR = [ + 'LFS2_TESTBD_BADBLOCK_PROGERROR', + 'LFS2_TESTBD_BADBLOCK_ERASEERROR', + 'LFS2_TESTBD_BADBLOCK_READERROR', + 'LFS2_TESTBD_BADBLOCK_PROGNOOP', + 'LFS2_TESTBD_BADBLOCK_ERASENOOP', +] +define.NAMEMULT = 64 +define.FILEMULT = 1 +code = ''' + for (lfs2_block_t i = 0; i < (LFS2_BLOCK_COUNT-2)/2; i++) { + lfs2_testbd_setwear(&cfg, (2*i) + 2, 0xffffffff) => 0; + } + + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 1; i < 10; i++) { + for (int j = 0; j < NAMEMULT; j++) { + buffer[j] = '0'+i; + } + buffer[NAMEMULT] = '\0'; + lfs2_mkdir(&lfs2, (char*)buffer) => 0; + + buffer[NAMEMULT] = '/'; + for (int j = 0; j < NAMEMULT; j++) { + buffer[j+NAMEMULT+1] = '0'+i; + } + buffer[2*NAMEMULT+1] = '\0'; + lfs2_file_open(&lfs2, &file, (char*)buffer, + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + + size = NAMEMULT; + for (int j = 0; j < i*FILEMULT; j++) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 1; i < 10; i++) { + for (int j = 0; j < NAMEMULT; j++) { + buffer[j] = '0'+i; + } + buffer[NAMEMULT] = '\0'; + lfs2_stat(&lfs2, (char*)buffer, &info) => 0; + info.type => LFS2_TYPE_DIR; + + buffer[NAMEMULT] = '/'; + for (int j = 0; j < NAMEMULT; j++) { + buffer[j+NAMEMULT+1] = '0'+i; + } + buffer[2*NAMEMULT+1] = '\0'; + lfs2_file_open(&lfs2, &file, (char*)buffer, LFS2_O_RDONLY) => 0; + + size = NAMEMULT; + for (int j = 0; j < i*FILEMULT; j++) { + uint8_t rbuffer[1024]; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(buffer, rbuffer, size) => 0; + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' + +# other corner cases +[[case]] # bad superblocks (corrupt 1 or 0) +define.LFS2_ERASE_CYCLES = 0xffffffff +define.LFS2_ERASE_VALUE = [0x00, 0xff, -1] +define.LFS2_BADBLOCK_BEHAVIOR = [ + 'LFS2_TESTBD_BADBLOCK_PROGERROR', + 'LFS2_TESTBD_BADBLOCK_ERASEERROR', + 'LFS2_TESTBD_BADBLOCK_READERROR', + 'LFS2_TESTBD_BADBLOCK_PROGNOOP', + 'LFS2_TESTBD_BADBLOCK_ERASENOOP', +] +code = ''' + lfs2_testbd_setwear(&cfg, 0, 0xffffffff) => 0; + lfs2_testbd_setwear(&cfg, 1, 0xffffffff) => 0; + + lfs2_format(&lfs2, &cfg) => LFS2_ERR_NOSPC; + lfs2_mount(&lfs2, &cfg) => LFS2_ERR_CORRUPT; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_dirs.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_dirs.toml new file mode 100644 index 00000000000..7d8eee4e59f --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_dirs.toml @@ -0,0 +1,838 @@ +[[case]] # root +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # many directory creation +define.N = 'range(0, 100, 3)' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "dir%03d", i); + lfs2_mkdir(&lfs2, path) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "dir%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # many directory removal +define.N = 'range(3, 100, 11)' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "removeme%03d", i); + lfs2_mkdir(&lfs2, path) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "removeme%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "removeme%03d", i); + lfs2_remove(&lfs2, path) => 0; + } + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # many directory rename +define.N = 'range(3, 100, 11)' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "test%03d", i); + lfs2_mkdir(&lfs2, path) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "test%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + char oldpath[128]; + char newpath[128]; + sprintf(oldpath, "test%03d", i); + sprintf(newpath, "tedd%03d", i); + lfs2_rename(&lfs2, oldpath, newpath) => 0; + } + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "tedd%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2); +''' + +[[case]] # reentrant many directory creation/rename/removal +define.N = [5, 11] +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + for (int i = 0; i < N; i++) { + sprintf(path, "hi%03d", i); + err = lfs2_mkdir(&lfs2, path); + assert(err == 0 || err == LFS2_ERR_EXIST); + } + + for (int i = 0; i < N; i++) { + sprintf(path, "hello%03d", i); + err = lfs2_remove(&lfs2, path); + assert(err == 0 || err == LFS2_ERR_NOENT); + } + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "hi%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + for (int i = 0; i < N; i++) { + char oldpath[128]; + char newpath[128]; + sprintf(oldpath, "hi%03d", i); + sprintf(newpath, "hello%03d", i); + // YES this can overwrite an existing newpath + lfs2_rename(&lfs2, oldpath, newpath) => 0; + } + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "hello%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + for (int i = 0; i < N; i++) { + sprintf(path, "hello%03d", i); + lfs2_remove(&lfs2, path) => 0; + } + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # file creation +define.N = 'range(3, 100, 11)' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "file%03d", i); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "file%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_REG); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2); +''' + +[[case]] # file removal +define.N = 'range(0, 100, 3)' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "removeme%03d", i); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "removeme%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_REG); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "removeme%03d", i); + lfs2_remove(&lfs2, path) => 0; + } + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # file rename +define.N = 'range(0, 100, 3)' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "test%03d", i); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "test%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_REG); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + char oldpath[128]; + char newpath[128]; + sprintf(oldpath, "test%03d", i); + sprintf(newpath, "tedd%03d", i); + lfs2_rename(&lfs2, oldpath, newpath) => 0; + } + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "tedd%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_REG); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2); +''' + +[[case]] # reentrant file creation/rename/removal +define.N = [5, 25] +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + for (int i = 0; i < N; i++) { + sprintf(path, "hi%03d", i); + lfs2_file_open(&lfs2, &file, path, LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + lfs2_file_close(&lfs2, &file) => 0; + } + + for (int i = 0; i < N; i++) { + sprintf(path, "hello%03d", i); + err = lfs2_remove(&lfs2, path); + assert(err == 0 || err == LFS2_ERR_NOENT); + } + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "hi%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_REG); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + for (int i = 0; i < N; i++) { + char oldpath[128]; + char newpath[128]; + sprintf(oldpath, "hi%03d", i); + sprintf(newpath, "hello%03d", i); + // YES this can overwrite an existing newpath + lfs2_rename(&lfs2, oldpath, newpath) => 0; + } + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "hello%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_REG); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + for (int i = 0; i < N; i++) { + sprintf(path, "hello%03d", i); + lfs2_remove(&lfs2, path) => 0; + } + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # nested directories +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "potato") => 0; + lfs2_file_open(&lfs2, &file, "burito", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "potato/baked") => 0; + lfs2_mkdir(&lfs2, "potato/sweet") => 0; + lfs2_mkdir(&lfs2, "potato/fried") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "potato") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "baked") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "fried") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "sweet") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + + // try removing? + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_remove(&lfs2, "potato") => LFS2_ERR_NOTEMPTY; + lfs2_unmount(&lfs2) => 0; + + // try renaming? + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "potato", "coldpotato") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "coldpotato", "warmpotato") => 0; + lfs2_rename(&lfs2, "warmpotato", "hotpotato") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_remove(&lfs2, "potato") => LFS2_ERR_NOENT; + lfs2_remove(&lfs2, "coldpotato") => LFS2_ERR_NOENT; + lfs2_remove(&lfs2, "warmpotato") => LFS2_ERR_NOENT; + lfs2_remove(&lfs2, "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_unmount(&lfs2) => 0; + + // try cross-directory renaming + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "coldpotato") => 0; + lfs2_rename(&lfs2, "hotpotato/baked", "coldpotato/baked") => 0; + lfs2_rename(&lfs2, "coldpotato", "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_remove(&lfs2, "coldpotato") => LFS2_ERR_NOTEMPTY; + lfs2_remove(&lfs2, "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_rename(&lfs2, "hotpotato/fried", "coldpotato/fried") => 0; + lfs2_rename(&lfs2, "coldpotato", "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_remove(&lfs2, "coldpotato") => LFS2_ERR_NOTEMPTY; + lfs2_remove(&lfs2, "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_rename(&lfs2, "hotpotato/sweet", "coldpotato/sweet") => 0; + lfs2_rename(&lfs2, "coldpotato", "hotpotato") => 0; + lfs2_remove(&lfs2, "coldpotato") => LFS2_ERR_NOENT; + lfs2_remove(&lfs2, "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "hotpotato") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "baked") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "fried") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "sweet") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + + // final remove + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_remove(&lfs2, "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_remove(&lfs2, "hotpotato/baked") => 0; + lfs2_remove(&lfs2, "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_remove(&lfs2, "hotpotato/fried") => 0; + lfs2_remove(&lfs2, "hotpotato") => LFS2_ERR_NOTEMPTY; + lfs2_remove(&lfs2, "hotpotato/sweet") => 0; + lfs2_remove(&lfs2, "hotpotato") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + info.type => LFS2_TYPE_DIR; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "burito") == 0); + info.type => LFS2_TYPE_REG; + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # recursive remove +define.N = [10, 100] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "prickly-pear") => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "prickly-pear/cactus%03d", i); + lfs2_mkdir(&lfs2, path) => 0; + } + lfs2_dir_open(&lfs2, &dir, "prickly-pear") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "cactus%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, path) == 0); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2); + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_remove(&lfs2, "prickly-pear") => LFS2_ERR_NOTEMPTY; + + lfs2_dir_open(&lfs2, &dir, "prickly-pear") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + for (int i = 0; i < N; i++) { + sprintf(path, "cactus%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, path) == 0); + sprintf(path, "prickly-pear/%s", info.name); + lfs2_remove(&lfs2, path) => 0; + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_remove(&lfs2, "prickly-pear") => 0; + lfs2_remove(&lfs2, "prickly-pear") => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_remove(&lfs2, "prickly-pear") => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # other error cases +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "potato") => 0; + lfs2_file_open(&lfs2, &file, "burito", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + + lfs2_mkdir(&lfs2, "potato") => LFS2_ERR_EXIST; + lfs2_mkdir(&lfs2, "burito") => LFS2_ERR_EXIST; + lfs2_file_open(&lfs2, &file, "burito", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => LFS2_ERR_EXIST; + lfs2_file_open(&lfs2, &file, "potato", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => LFS2_ERR_EXIST; + lfs2_dir_open(&lfs2, &dir, "tomato") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "burito") => LFS2_ERR_NOTDIR; + lfs2_file_open(&lfs2, &file, "tomato", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "potato", LFS2_O_RDONLY) => LFS2_ERR_ISDIR; + lfs2_file_open(&lfs2, &file, "tomato", LFS2_O_WRONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "potato", LFS2_O_WRONLY) => LFS2_ERR_ISDIR; + lfs2_file_open(&lfs2, &file, "potato", + LFS2_O_WRONLY | LFS2_O_CREAT) => LFS2_ERR_ISDIR; + + lfs2_mkdir(&lfs2, "/") => LFS2_ERR_EXIST; + lfs2_file_open(&lfs2, &file, "/", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => LFS2_ERR_EXIST; + lfs2_file_open(&lfs2, &file, "/", LFS2_O_RDONLY) => LFS2_ERR_ISDIR; + lfs2_file_open(&lfs2, &file, "/", LFS2_O_WRONLY) => LFS2_ERR_ISDIR; + lfs2_file_open(&lfs2, &file, "/", + LFS2_O_WRONLY | LFS2_O_CREAT) => LFS2_ERR_ISDIR; + + // check that errors did not corrupt directory + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_REG); + assert(strcmp(info.name, "burito") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "potato") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_unmount(&lfs2) => 0; + + // or on disk + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, ".") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "..") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_REG); + assert(strcmp(info.name, "burito") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(info.type == LFS2_TYPE_DIR); + assert(strcmp(info.name, "potato") == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # directory seek +define.COUNT = [4, 128, 132] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "hello") => 0; + for (int i = 0; i < COUNT; i++) { + sprintf(path, "hello/kitty%03d", i); + lfs2_mkdir(&lfs2, path) => 0; + } + lfs2_unmount(&lfs2) => 0; + + for (int j = 2; j < COUNT; j++) { + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "hello") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_soff_t pos; + for (int i = 0; i < j; i++) { + sprintf(path, "kitty%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_DIR); + pos = lfs2_dir_tell(&lfs2, &dir); + assert(pos >= 0); + } + + lfs2_dir_seek(&lfs2, &dir, pos) => 0; + sprintf(path, "kitty%03d", j); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_dir_rewind(&lfs2, &dir) => 0; + sprintf(path, "kitty%03d", 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_dir_seek(&lfs2, &dir, pos) => 0; + sprintf(path, "kitty%03d", j); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + } +''' + +[[case]] # root seek +define.COUNT = [4, 128, 132] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < COUNT; i++) { + sprintf(path, "hi%03d", i); + lfs2_mkdir(&lfs2, path) => 0; + } + lfs2_unmount(&lfs2) => 0; + + for (int j = 2; j < COUNT; j++) { + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_soff_t pos; + for (int i = 0; i < j; i++) { + sprintf(path, "hi%03d", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_DIR); + pos = lfs2_dir_tell(&lfs2, &dir); + assert(pos >= 0); + } + + lfs2_dir_seek(&lfs2, &dir, pos) => 0; + sprintf(path, "hi%03d", j); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_dir_rewind(&lfs2, &dir) => 0; + sprintf(path, "hi%03d", 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_dir_seek(&lfs2, &dir, pos) => 0; + sprintf(path, "hi%03d", j); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + } +''' + diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_entries.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_entries.toml new file mode 100644 index 00000000000..c8b2e5d1c5f --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_entries.toml @@ -0,0 +1,611 @@ +# These tests are for some specific corner cases with neighboring inline files. +# Note that these tests are intended for 512 byte inline sizes. They should +# still pass with other inline sizes but wouldn't be testing anything. + +define.LFS2_CACHE_SIZE = 512 +if = 'LFS2_CACHE_SIZE % LFS2_PROG_SIZE == 0 && LFS2_CACHE_SIZE == 512' + +[[case]] # entry grow test +code = ''' + uint8_t wbuffer[1024]; + uint8_t rbuffer[1024]; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // write hi0 20 + sprintf(path, "hi0"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 20 + sprintf(path, "hi1"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi2 20 + sprintf(path, "hi2"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi3 20 + sprintf(path, "hi3"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi1 20 + sprintf(path, "hi1"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi0 20 + sprintf(path, "hi0"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi2 20 + sprintf(path, "hi2"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi3 20 + sprintf(path, "hi3"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # entry shrink test +code = ''' + uint8_t wbuffer[1024]; + uint8_t rbuffer[1024]; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // write hi0 20 + sprintf(path, "hi0"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi2 20 + sprintf(path, "hi2"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi3 20 + sprintf(path, "hi3"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 20 + sprintf(path, "hi1"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi0 20 + sprintf(path, "hi0"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi1 20 + sprintf(path, "hi1"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi2 20 + sprintf(path, "hi2"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi3 20 + sprintf(path, "hi3"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # entry spill test +code = ''' + uint8_t wbuffer[1024]; + uint8_t rbuffer[1024]; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // write hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi2 200 + sprintf(path, "hi2"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi2 200 + sprintf(path, "hi2"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # entry push spill test +code = ''' + uint8_t wbuffer[1024]; + uint8_t rbuffer[1024]; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // write hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 20 + sprintf(path, "hi1"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi2 200 + sprintf(path, "hi2"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi1 20 + sprintf(path, "hi1"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi2 200 + sprintf(path, "hi2"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # entry push spill two test +code = ''' + uint8_t wbuffer[1024]; + uint8_t rbuffer[1024]; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // write hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 20 + sprintf(path, "hi1"); size = 20; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi2 200 + sprintf(path, "hi2"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi4 200 + sprintf(path, "hi4"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi1 20 + sprintf(path, "hi1"); size = 20; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + // read hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi2 200 + sprintf(path, "hi2"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi4 200 + sprintf(path, "hi4"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # entry drop test +code = ''' + uint8_t wbuffer[1024]; + uint8_t rbuffer[1024]; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // write hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi1 200 + sprintf(path, "hi1"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi2 200 + sprintf(path, "hi2"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + // write hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_remove(&lfs2, "hi1") => 0; + lfs2_stat(&lfs2, "hi1", &info) => LFS2_ERR_NOENT; + // read hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi2 200 + sprintf(path, "hi2"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_remove(&lfs2, "hi2") => 0; + lfs2_stat(&lfs2, "hi2", &info) => LFS2_ERR_NOENT; + // read hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + // read hi3 200 + sprintf(path, "hi3"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_remove(&lfs2, "hi3") => 0; + lfs2_stat(&lfs2, "hi3", &info) => LFS2_ERR_NOENT; + // read hi0 200 + sprintf(path, "hi0"); size = 200; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_remove(&lfs2, "hi0") => 0; + lfs2_stat(&lfs2, "hi0", &info) => LFS2_ERR_NOENT; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # create too big +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + memset(path, 'm', 200); + path[200] = '\0'; + + size = 400; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + uint8_t wbuffer[1024]; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_close(&lfs2, &file) => 0; + + size = 400; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + uint8_t rbuffer[1024]; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # resize too big +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + memset(path, 'm', 200); + path[200] = '\0'; + + size = 40; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + uint8_t wbuffer[1024]; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_close(&lfs2, &file) => 0; + + size = 40; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + uint8_t rbuffer[1024]; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + size = 400; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + memset(wbuffer, 'c', size); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_close(&lfs2, &file) => 0; + + size = 400; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + memcmp(rbuffer, wbuffer, size) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_evil.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_evil.toml new file mode 100644 index 00000000000..bdf2731eb14 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_evil.toml @@ -0,0 +1,288 @@ +# Tests for recovering from conditions which shouldn't normally +# happen during normal operation of littlefs + +# invalid pointer tests (outside of block_count) + +[[case]] # invalid tail-pointer test +define.TAIL_TYPE = ['LFS2_TYPE_HARDTAIL', 'LFS2_TYPE_SOFTTAIL'] +define.INVALSET = [0x3, 0x1, 0x2] +in = "lfs2.c" +code = ''' + // create littlefs + lfs2_format(&lfs2, &cfg) => 0; + + // change tail-pointer to invalid pointers + lfs2_init(&lfs2, &cfg) => 0; + lfs2_mdir_t mdir; + lfs2_dir_fetch(&lfs2, &mdir, (lfs2_block_t[2]){0, 1}) => 0; + lfs2_dir_commit(&lfs2, &mdir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_HARDTAIL, 0x3ff, 8), + (lfs2_block_t[2]){ + (INVALSET & 0x1) ? 0xcccccccc : 0, + (INVALSET & 0x2) ? 0xcccccccc : 0}})) => 0; + lfs2_deinit(&lfs2) => 0; + + // test that mount fails gracefully + lfs2_mount(&lfs2, &cfg) => LFS2_ERR_CORRUPT; +''' + +[[case]] # invalid dir pointer test +define.INVALSET = [0x3, 0x1, 0x2] +in = "lfs2.c" +code = ''' + // create littlefs + lfs2_format(&lfs2, &cfg) => 0; + // make a dir + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "dir_here") => 0; + lfs2_unmount(&lfs2) => 0; + + // change the dir pointer to be invalid + lfs2_init(&lfs2, &cfg) => 0; + lfs2_mdir_t mdir; + lfs2_dir_fetch(&lfs2, &mdir, (lfs2_block_t[2]){0, 1}) => 0; + // make sure id 1 == our directory + lfs2_dir_get(&lfs2, &mdir, + LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_NAME, 1, strlen("dir_here")), buffer) + => LFS2_MKTAG(LFS2_TYPE_DIR, 1, strlen("dir_here")); + assert(memcmp((char*)buffer, "dir_here", strlen("dir_here")) == 0); + // change dir pointer + lfs2_dir_commit(&lfs2, &mdir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_DIRSTRUCT, 1, 8), + (lfs2_block_t[2]){ + (INVALSET & 0x1) ? 0xcccccccc : 0, + (INVALSET & 0x2) ? 0xcccccccc : 0}})) => 0; + lfs2_deinit(&lfs2) => 0; + + // test that accessing our bad dir fails, note there's a number + // of ways to access the dir, some can fail, but some don't + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "dir_here", &info) => 0; + assert(strcmp(info.name, "dir_here") == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_dir_open(&lfs2, &dir, "dir_here") => LFS2_ERR_CORRUPT; + lfs2_stat(&lfs2, "dir_here/file_here", &info) => LFS2_ERR_CORRUPT; + lfs2_dir_open(&lfs2, &dir, "dir_here/dir_here") => LFS2_ERR_CORRUPT; + lfs2_file_open(&lfs2, &file, "dir_here/file_here", + LFS2_O_RDONLY) => LFS2_ERR_CORRUPT; + lfs2_file_open(&lfs2, &file, "dir_here/file_here", + LFS2_O_WRONLY | LFS2_O_CREAT) => LFS2_ERR_CORRUPT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # invalid file pointer test +in = "lfs2.c" +define.SIZE = [10, 1000, 100000] # faked file size +code = ''' + // create littlefs + lfs2_format(&lfs2, &cfg) => 0; + // make a file + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "file_here", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // change the file pointer to be invalid + lfs2_init(&lfs2, &cfg) => 0; + lfs2_mdir_t mdir; + lfs2_dir_fetch(&lfs2, &mdir, (lfs2_block_t[2]){0, 1}) => 0; + // make sure id 1 == our file + lfs2_dir_get(&lfs2, &mdir, + LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_NAME, 1, strlen("file_here")), buffer) + => LFS2_MKTAG(LFS2_TYPE_REG, 1, strlen("file_here")); + assert(memcmp((char*)buffer, "file_here", strlen("file_here")) == 0); + // change file pointer + lfs2_dir_commit(&lfs2, &mdir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_CTZSTRUCT, 1, sizeof(struct lfs2_ctz)), + &(struct lfs2_ctz){0xcccccccc, lfs2_tole32(SIZE)}})) => 0; + lfs2_deinit(&lfs2) => 0; + + // test that accessing our bad file fails, note there's a number + // of ways to access the dir, some can fail, but some don't + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "file_here", &info) => 0; + assert(strcmp(info.name, "file_here") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == SIZE); + + lfs2_file_open(&lfs2, &file, "file_here", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, SIZE) => LFS2_ERR_CORRUPT; + lfs2_file_close(&lfs2, &file) => 0; + + // any allocs that traverse CTZ must unfortunately must fail + if (SIZE > 2*LFS2_BLOCK_SIZE) { + lfs2_mkdir(&lfs2, "dir_here") => LFS2_ERR_CORRUPT; + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # invalid pointer in CTZ skip-list test +define.SIZE = ['2*LFS2_BLOCK_SIZE', '3*LFS2_BLOCK_SIZE', '4*LFS2_BLOCK_SIZE'] +in = "lfs2.c" +code = ''' + // create littlefs + lfs2_format(&lfs2, &cfg) => 0; + // make a file + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "file_here", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + for (int i = 0; i < SIZE; i++) { + char c = 'c'; + lfs2_file_write(&lfs2, &file, &c, 1) => 1; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + // change pointer in CTZ skip-list to be invalid + lfs2_init(&lfs2, &cfg) => 0; + lfs2_mdir_t mdir; + lfs2_dir_fetch(&lfs2, &mdir, (lfs2_block_t[2]){0, 1}) => 0; + // make sure id 1 == our file and get our CTZ structure + lfs2_dir_get(&lfs2, &mdir, + LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_NAME, 1, strlen("file_here")), buffer) + => LFS2_MKTAG(LFS2_TYPE_REG, 1, strlen("file_here")); + assert(memcmp((char*)buffer, "file_here", strlen("file_here")) == 0); + struct lfs2_ctz ctz; + lfs2_dir_get(&lfs2, &mdir, + LFS2_MKTAG(0x700, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_STRUCT, 1, sizeof(struct lfs2_ctz)), &ctz) + => LFS2_MKTAG(LFS2_TYPE_CTZSTRUCT, 1, sizeof(struct lfs2_ctz)); + lfs2_ctz_fromle32(&ctz); + // rewrite block to contain bad pointer + uint8_t bbuffer[LFS2_BLOCK_SIZE]; + cfg.read(&cfg, ctz.head, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + uint32_t bad = lfs2_tole32(0xcccccccc); + memcpy(&bbuffer[0], &bad, sizeof(bad)); + memcpy(&bbuffer[4], &bad, sizeof(bad)); + cfg.erase(&cfg, ctz.head) => 0; + cfg.prog(&cfg, ctz.head, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + lfs2_deinit(&lfs2) => 0; + + // test that accessing our bad file fails, note there's a number + // of ways to access the dir, some can fail, but some don't + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "file_here", &info) => 0; + assert(strcmp(info.name, "file_here") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == SIZE); + + lfs2_file_open(&lfs2, &file, "file_here", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, SIZE) => LFS2_ERR_CORRUPT; + lfs2_file_close(&lfs2, &file) => 0; + + // any allocs that traverse CTZ must unfortunately must fail + if (SIZE > 2*LFS2_BLOCK_SIZE) { + lfs2_mkdir(&lfs2, "dir_here") => LFS2_ERR_CORRUPT; + } + lfs2_unmount(&lfs2) => 0; +''' + + +[[case]] # invalid gstate pointer +define.INVALSET = [0x3, 0x1, 0x2] +in = "lfs2.c" +code = ''' + // create littlefs + lfs2_format(&lfs2, &cfg) => 0; + + // create an invalid gstate + lfs2_init(&lfs2, &cfg) => 0; + lfs2_mdir_t mdir; + lfs2_dir_fetch(&lfs2, &mdir, (lfs2_block_t[2]){0, 1}) => 0; + lfs2_fs_prepmove(&lfs2, 1, (lfs2_block_t [2]){ + (INVALSET & 0x1) ? 0xcccccccc : 0, + (INVALSET & 0x2) ? 0xcccccccc : 0}); + lfs2_dir_commit(&lfs2, &mdir, NULL, 0) => 0; + lfs2_deinit(&lfs2) => 0; + + // test that mount fails gracefully + // mount may not fail, but our first alloc should fail when + // we try to fix the gstate + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "should_fail") => LFS2_ERR_CORRUPT; + lfs2_unmount(&lfs2) => 0; +''' + +# cycle detection/recovery tests + +[[case]] # metadata-pair threaded-list loop test +in = "lfs2.c" +code = ''' + // create littlefs + lfs2_format(&lfs2, &cfg) => 0; + + // change tail-pointer to point to ourself + lfs2_init(&lfs2, &cfg) => 0; + lfs2_mdir_t mdir; + lfs2_dir_fetch(&lfs2, &mdir, (lfs2_block_t[2]){0, 1}) => 0; + lfs2_dir_commit(&lfs2, &mdir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_HARDTAIL, 0x3ff, 8), + (lfs2_block_t[2]){0, 1}})) => 0; + lfs2_deinit(&lfs2) => 0; + + // test that mount fails gracefully + lfs2_mount(&lfs2, &cfg) => LFS2_ERR_CORRUPT; +''' + +[[case]] # metadata-pair threaded-list 2-length loop test +in = "lfs2.c" +code = ''' + // create littlefs with child dir + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "child") => 0; + lfs2_unmount(&lfs2) => 0; + + // find child + lfs2_init(&lfs2, &cfg) => 0; + lfs2_mdir_t mdir; + lfs2_block_t pair[2]; + lfs2_dir_fetch(&lfs2, &mdir, (lfs2_block_t[2]){0, 1}) => 0; + lfs2_dir_get(&lfs2, &mdir, + LFS2_MKTAG(0x7ff, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_DIRSTRUCT, 1, sizeof(pair)), pair) + => LFS2_MKTAG(LFS2_TYPE_DIRSTRUCT, 1, sizeof(pair)); + lfs2_pair_fromle32(pair); + // change tail-pointer to point to root + lfs2_dir_fetch(&lfs2, &mdir, pair) => 0; + lfs2_dir_commit(&lfs2, &mdir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_HARDTAIL, 0x3ff, 8), + (lfs2_block_t[2]){0, 1}})) => 0; + lfs2_deinit(&lfs2) => 0; + + // test that mount fails gracefully + lfs2_mount(&lfs2, &cfg) => LFS2_ERR_CORRUPT; +''' + +[[case]] # metadata-pair threaded-list 1-length child loop test +in = "lfs2.c" +code = ''' + // create littlefs with child dir + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "child") => 0; + lfs2_unmount(&lfs2) => 0; + + // find child + lfs2_init(&lfs2, &cfg) => 0; + lfs2_mdir_t mdir; + lfs2_block_t pair[2]; + lfs2_dir_fetch(&lfs2, &mdir, (lfs2_block_t[2]){0, 1}) => 0; + lfs2_dir_get(&lfs2, &mdir, + LFS2_MKTAG(0x7ff, 0x3ff, 0), + LFS2_MKTAG(LFS2_TYPE_DIRSTRUCT, 1, sizeof(pair)), pair) + => LFS2_MKTAG(LFS2_TYPE_DIRSTRUCT, 1, sizeof(pair)); + lfs2_pair_fromle32(pair); + // change tail-pointer to point to ourself + lfs2_dir_fetch(&lfs2, &mdir, pair) => 0; + lfs2_dir_commit(&lfs2, &mdir, LFS2_MKATTRS( + {LFS2_MKTAG(LFS2_TYPE_HARDTAIL, 0x3ff, 8), pair})) => 0; + lfs2_deinit(&lfs2) => 0; + + // test that mount fails gracefully + lfs2_mount(&lfs2, &cfg) => LFS2_ERR_CORRUPT; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_exhaustion.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_exhaustion.toml new file mode 100644 index 00000000000..6d98c875288 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_exhaustion.toml @@ -0,0 +1,465 @@ +[[case]] # test running a filesystem to exhaustion +define.LFS2_ERASE_CYCLES = 10 +define.LFS2_BLOCK_COUNT = 256 # small bd so test runs faster +define.LFS2_BLOCK_CYCLES = 'LFS2_ERASE_CYCLES / 2' +define.LFS2_BADBLOCK_BEHAVIOR = [ + 'LFS2_TESTBD_BADBLOCK_PROGERROR', + 'LFS2_TESTBD_BADBLOCK_ERASEERROR', + 'LFS2_TESTBD_BADBLOCK_READERROR', + 'LFS2_TESTBD_BADBLOCK_PROGNOOP', + 'LFS2_TESTBD_BADBLOCK_ERASENOOP', +] +define.FILES = 10 +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "roadrunner") => 0; + lfs2_unmount(&lfs2) => 0; + + uint32_t cycle = 0; + while (true) { + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // chose name, roughly random seed, and random 2^n size + sprintf(path, "roadrunner/test%d", i); + srand(cycle * i); + size = 1 << ((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + lfs2_ssize_t res = lfs2_file_write(&lfs2, &file, &c, 1); + assert(res == 1 || res == LFS2_ERR_NOSPC); + if (res == LFS2_ERR_NOSPC) { + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + if (err == LFS2_ERR_NOSPC) { + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "roadrunner/test%d", i); + srand(cycle * i); + size = 1 << ((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + char r; + lfs2_file_read(&lfs2, &file, &r, 1) => 1; + assert(r == c); + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + cycle += 1; + } + +exhausted: + // should still be readable + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "roadrunner/test%d", i); + lfs2_stat(&lfs2, path, &info) => 0; + } + lfs2_unmount(&lfs2) => 0; + + LFS2_WARN("completed %d cycles", cycle); +''' + +[[case]] # test running a filesystem to exhaustion + # which also requires expanding superblocks +define.LFS2_ERASE_CYCLES = 10 +define.LFS2_BLOCK_COUNT = 256 # small bd so test runs faster +define.LFS2_BLOCK_CYCLES = 'LFS2_ERASE_CYCLES / 2' +define.LFS2_BADBLOCK_BEHAVIOR = [ + 'LFS2_TESTBD_BADBLOCK_PROGERROR', + 'LFS2_TESTBD_BADBLOCK_ERASEERROR', + 'LFS2_TESTBD_BADBLOCK_READERROR', + 'LFS2_TESTBD_BADBLOCK_PROGNOOP', + 'LFS2_TESTBD_BADBLOCK_ERASENOOP', +] +define.FILES = 10 +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + uint32_t cycle = 0; + while (true) { + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // chose name, roughly random seed, and random 2^n size + sprintf(path, "test%d", i); + srand(cycle * i); + size = 1 << ((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + lfs2_ssize_t res = lfs2_file_write(&lfs2, &file, &c, 1); + assert(res == 1 || res == LFS2_ERR_NOSPC); + if (res == LFS2_ERR_NOSPC) { + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + if (err == LFS2_ERR_NOSPC) { + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "test%d", i); + srand(cycle * i); + size = 1 << ((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + char r; + lfs2_file_read(&lfs2, &file, &r, 1) => 1; + assert(r == c); + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + cycle += 1; + } + +exhausted: + // should still be readable + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "test%d", i); + lfs2_stat(&lfs2, path, &info) => 0; + } + lfs2_unmount(&lfs2) => 0; + + LFS2_WARN("completed %d cycles", cycle); +''' + +# These are a sort of high-level litmus test for wear-leveling. One definition +# of wear-leveling is that increasing a block device's space translates directly +# into increasing the block devices lifetime. This is something we can actually +# check for. + +[[case]] # wear-level test running a filesystem to exhaustion +define.LFS2_ERASE_CYCLES = 20 +define.LFS2_BLOCK_COUNT = 256 # small bd so test runs faster +define.LFS2_BLOCK_CYCLES = 'LFS2_ERASE_CYCLES / 2' +define.FILES = 10 +code = ''' + uint32_t run_cycles[2]; + const uint32_t run_block_count[2] = {LFS2_BLOCK_COUNT/2, LFS2_BLOCK_COUNT}; + + for (int run = 0; run < 2; run++) { + for (lfs2_block_t b = 0; b < LFS2_BLOCK_COUNT; b++) { + lfs2_testbd_setwear(&cfg, b, + (b < run_block_count[run]) ? 0 : LFS2_ERASE_CYCLES) => 0; + } + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "roadrunner") => 0; + lfs2_unmount(&lfs2) => 0; + + uint32_t cycle = 0; + while (true) { + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // chose name, roughly random seed, and random 2^n size + sprintf(path, "roadrunner/test%d", i); + srand(cycle * i); + size = 1 << ((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + lfs2_ssize_t res = lfs2_file_write(&lfs2, &file, &c, 1); + assert(res == 1 || res == LFS2_ERR_NOSPC); + if (res == LFS2_ERR_NOSPC) { + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + if (err == LFS2_ERR_NOSPC) { + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "roadrunner/test%d", i); + srand(cycle * i); + size = 1 << ((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + char r; + lfs2_file_read(&lfs2, &file, &r, 1) => 1; + assert(r == c); + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + cycle += 1; + } + +exhausted: + // should still be readable + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "roadrunner/test%d", i); + lfs2_stat(&lfs2, path, &info) => 0; + } + lfs2_unmount(&lfs2) => 0; + + run_cycles[run] = cycle; + LFS2_WARN("completed %d blocks %d cycles", + run_block_count[run], run_cycles[run]); + } + + // check we increased the lifetime by 2x with ~10% error + LFS2_ASSERT(run_cycles[1]*110/100 > 2*run_cycles[0]); +''' + +[[case]] # wear-level test + expanding superblock +define.LFS2_ERASE_CYCLES = 20 +define.LFS2_BLOCK_COUNT = 256 # small bd so test runs faster +define.LFS2_BLOCK_CYCLES = 'LFS2_ERASE_CYCLES / 2' +define.FILES = 10 +code = ''' + uint32_t run_cycles[2]; + const uint32_t run_block_count[2] = {LFS2_BLOCK_COUNT/2, LFS2_BLOCK_COUNT}; + + for (int run = 0; run < 2; run++) { + for (lfs2_block_t b = 0; b < LFS2_BLOCK_COUNT; b++) { + lfs2_testbd_setwear(&cfg, b, + (b < run_block_count[run]) ? 0 : LFS2_ERASE_CYCLES) => 0; + } + + lfs2_format(&lfs2, &cfg) => 0; + + uint32_t cycle = 0; + while (true) { + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // chose name, roughly random seed, and random 2^n size + sprintf(path, "test%d", i); + srand(cycle * i); + size = 1 << ((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + lfs2_ssize_t res = lfs2_file_write(&lfs2, &file, &c, 1); + assert(res == 1 || res == LFS2_ERR_NOSPC); + if (res == LFS2_ERR_NOSPC) { + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + if (err == LFS2_ERR_NOSPC) { + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "test%d", i); + srand(cycle * i); + size = 1 << ((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + char r; + lfs2_file_read(&lfs2, &file, &r, 1) => 1; + assert(r == c); + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + cycle += 1; + } + +exhausted: + // should still be readable + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "test%d", i); + lfs2_stat(&lfs2, path, &info) => 0; + } + lfs2_unmount(&lfs2) => 0; + + run_cycles[run] = cycle; + LFS2_WARN("completed %d blocks %d cycles", + run_block_count[run], run_cycles[run]); + } + + // check we increased the lifetime by 2x with ~10% error + LFS2_ASSERT(run_cycles[1]*110/100 > 2*run_cycles[0]); +''' + +[[case]] # test that we wear blocks roughly evenly +define.LFS2_ERASE_CYCLES = 0xffffffff +define.LFS2_BLOCK_COUNT = 256 # small bd so test runs faster +define.LFS2_BLOCK_CYCLES = [5, 4, 3, 2, 1] +define.CYCLES = 100 +define.FILES = 10 +if = 'LFS2_BLOCK_CYCLES < CYCLES/10' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "roadrunner") => 0; + lfs2_unmount(&lfs2) => 0; + + uint32_t cycle = 0; + while (cycle < CYCLES) { + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // chose name, roughly random seed, and random 2^n size + sprintf(path, "roadrunner/test%d", i); + srand(cycle * i); + size = 1 << 4; //((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + lfs2_ssize_t res = lfs2_file_write(&lfs2, &file, &c, 1); + assert(res == 1 || res == LFS2_ERR_NOSPC); + if (res == LFS2_ERR_NOSPC) { + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + err = lfs2_file_close(&lfs2, &file); + assert(err == 0 || err == LFS2_ERR_NOSPC); + if (err == LFS2_ERR_NOSPC) { + lfs2_unmount(&lfs2) => 0; + goto exhausted; + } + } + + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "roadrunner/test%d", i); + srand(cycle * i); + size = 1 << 4; //((rand() % 10)+2); + + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + for (lfs2_size_t j = 0; j < size; j++) { + char c = 'a' + (rand() % 26); + char r; + lfs2_file_read(&lfs2, &file, &r, 1) => 1; + assert(r == c); + } + + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + + cycle += 1; + } + +exhausted: + // should still be readable + lfs2_mount(&lfs2, &cfg) => 0; + for (uint32_t i = 0; i < FILES; i++) { + // check for errors + sprintf(path, "roadrunner/test%d", i); + lfs2_stat(&lfs2, path, &info) => 0; + } + lfs2_unmount(&lfs2) => 0; + + LFS2_WARN("completed %d cycles", cycle); + + // check the wear on our block device + lfs2_testbd_wear_t minwear = -1; + lfs2_testbd_wear_t totalwear = 0; + lfs2_testbd_wear_t maxwear = 0; + // skip 0 and 1 as superblock movement is intentionally avoided + for (lfs2_block_t b = 2; b < LFS2_BLOCK_COUNT; b++) { + lfs2_testbd_wear_t wear = lfs2_testbd_getwear(&cfg, b); + printf("%08x: wear %d\n", b, wear); + assert(wear >= 0); + if (wear < minwear) { + minwear = wear; + } + if (wear > maxwear) { + maxwear = wear; + } + totalwear += wear; + } + lfs2_testbd_wear_t avgwear = totalwear / LFS2_BLOCK_COUNT; + LFS2_WARN("max wear: %d cycles", maxwear); + LFS2_WARN("avg wear: %d cycles", totalwear / LFS2_BLOCK_COUNT); + LFS2_WARN("min wear: %d cycles", minwear); + + // find standard deviation^2 + lfs2_testbd_wear_t dev2 = 0; + for (lfs2_block_t b = 2; b < LFS2_BLOCK_COUNT; b++) { + lfs2_testbd_wear_t wear = lfs2_testbd_getwear(&cfg, b); + assert(wear >= 0); + lfs2_testbd_swear_t diff = wear - avgwear; + dev2 += diff*diff; + } + dev2 /= totalwear; + LFS2_WARN("std dev^2: %d", dev2); + assert(dev2 < 8); +''' + diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_files.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_files.toml new file mode 100644 index 00000000000..9e6cab6cc2d --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_files.toml @@ -0,0 +1,486 @@ + +[[case]] # simple file test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "hello", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + size = strlen("Hello World!")+1; + strcpy((char*)buffer, "Hello World!"); + lfs2_file_write(&lfs2, &file, buffer, size) => size; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(strcmp((char*)buffer, "Hello World!") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # larger files +define.SIZE = [32, 8192, 262144, 0, 7, 8193] +define.CHUNKSIZE = [31, 16, 33, 1, 1023] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + // write + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + srand(1); + for (lfs2_size_t i = 0; i < SIZE; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // read + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => SIZE; + srand(1); + for (lfs2_size_t i = 0; i < SIZE; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # rewriting files +define.SIZE1 = [32, 8192, 131072, 0, 7, 8193] +define.SIZE2 = [32, 8192, 131072, 0, 7, 8193] +define.CHUNKSIZE = [31, 16, 1] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + // write + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + srand(1); + for (lfs2_size_t i = 0; i < SIZE1; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE1-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // read + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => SIZE1; + srand(1); + for (lfs2_size_t i = 0; i < SIZE1; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE1-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // rewrite + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_WRONLY) => 0; + srand(2); + for (lfs2_size_t i = 0; i < SIZE2; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE2-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // read + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => lfs2_max(SIZE1, SIZE2); + srand(2); + for (lfs2_size_t i = 0; i < SIZE2; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE2-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + if (SIZE1 > SIZE2) { + srand(1); + for (lfs2_size_t b = 0; b < SIZE2; b++) { + rand(); + } + for (lfs2_size_t i = SIZE2; i < SIZE1; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE1-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # appending files +define.SIZE1 = [32, 8192, 131072, 0, 7, 8193] +define.SIZE2 = [32, 8192, 131072, 0, 7, 8193] +define.CHUNKSIZE = [31, 16, 1] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + // write + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + srand(1); + for (lfs2_size_t i = 0; i < SIZE1; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE1-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // read + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => SIZE1; + srand(1); + for (lfs2_size_t i = 0; i < SIZE1; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE1-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // append + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_WRONLY | LFS2_O_APPEND) => 0; + srand(2); + for (lfs2_size_t i = 0; i < SIZE2; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE2-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // read + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => SIZE1 + SIZE2; + srand(1); + for (lfs2_size_t i = 0; i < SIZE1; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE1-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + srand(2); + for (lfs2_size_t i = 0; i < SIZE2; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE2-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # truncating files +define.SIZE1 = [32, 8192, 131072, 0, 7, 8193] +define.SIZE2 = [32, 8192, 131072, 0, 7, 8193] +define.CHUNKSIZE = [31, 16, 1] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + + // write + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + srand(1); + for (lfs2_size_t i = 0; i < SIZE1; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE1-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // read + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => SIZE1; + srand(1); + for (lfs2_size_t i = 0; i < SIZE1; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE1-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // truncate + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + srand(2); + for (lfs2_size_t i = 0; i < SIZE2; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE2-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + // read + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => SIZE2; + srand(2); + for (lfs2_size_t i = 0; i < SIZE2; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE2-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # reentrant file writing +define.SIZE = [32, 0, 7, 2049] +define.CHUNKSIZE = [31, 16, 65] +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + err = lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY); + assert(err == LFS2_ERR_NOENT || err == 0); + if (err == 0) { + // can only be 0 (new file) or full size + size = lfs2_file_size(&lfs2, &file); + assert(size == 0 || size == SIZE); + lfs2_file_close(&lfs2, &file) => 0; + } + + // write + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + srand(1); + for (lfs2_size_t i = 0; i < SIZE; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + } + lfs2_file_close(&lfs2, &file) => 0; + + // read + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => SIZE; + srand(1); + for (lfs2_size_t i = 0; i < SIZE; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # reentrant file writing with syncs +define = [ + # append (O(n)) + {MODE='LFS2_O_APPEND', SIZE=[32, 0, 7, 2049], CHUNKSIZE=[31, 16, 65]}, + # truncate (O(n^2)) + {MODE='LFS2_O_TRUNC', SIZE=[32, 0, 7, 200], CHUNKSIZE=[31, 16, 65]}, + # rewrite (O(n^2)) + {MODE=0, SIZE=[32, 0, 7, 200], CHUNKSIZE=[31, 16, 65]}, +] +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + err = lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY); + assert(err == LFS2_ERR_NOENT || err == 0); + if (err == 0) { + // with syncs we could be any size, but it at least must be valid data + size = lfs2_file_size(&lfs2, &file); + assert(size <= SIZE); + srand(1); + for (lfs2_size_t i = 0; i < size; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, size-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_close(&lfs2, &file) => 0; + } + + // write + lfs2_file_open(&lfs2, &file, "avacado", + LFS2_O_WRONLY | LFS2_O_CREAT | MODE) => 0; + size = lfs2_file_size(&lfs2, &file); + assert(size <= SIZE); + srand(1); + lfs2_size_t skip = (MODE == LFS2_O_APPEND) ? size : 0; + for (lfs2_size_t b = 0; b < skip; b++) { + rand(); + } + for (lfs2_size_t i = skip; i < SIZE; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE-i); + for (lfs2_size_t b = 0; b < chunk; b++) { + buffer[b] = rand() & 0xff; + } + lfs2_file_write(&lfs2, &file, buffer, chunk) => chunk; + lfs2_file_sync(&lfs2, &file) => 0; + } + lfs2_file_close(&lfs2, &file) => 0; + + // read + lfs2_file_open(&lfs2, &file, "avacado", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => SIZE; + srand(1); + for (lfs2_size_t i = 0; i < SIZE; i += CHUNKSIZE) { + lfs2_size_t chunk = lfs2_min(CHUNKSIZE, SIZE-i); + lfs2_file_read(&lfs2, &file, buffer, chunk) => chunk; + for (lfs2_size_t b = 0; b < chunk; b++) { + assert(buffer[b] == (rand() & 0xff)); + } + } + lfs2_file_read(&lfs2, &file, buffer, CHUNKSIZE) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # many files +define.N = 300 +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + // create N files of 7 bytes + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "file_%03d", i); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + char wbuffer[1024]; + size = 7; + snprintf(wbuffer, size, "Hi %03d", i); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_close(&lfs2, &file) => 0; + + char rbuffer[1024]; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + assert(strcmp(rbuffer, wbuffer) == 0); + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # many files with power cycle +define.N = 300 +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + // create N files of 7 bytes + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + sprintf(path, "file_%03d", i); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + char wbuffer[1024]; + size = 7; + snprintf(wbuffer, size, "Hi %03d", i); + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + char rbuffer[1024]; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + assert(strcmp(rbuffer, wbuffer) == 0); + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # many files with power loss +define.N = 300 +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + // create N files of 7 bytes + for (int i = 0; i < N; i++) { + sprintf(path, "file_%03d", i); + err = lfs2_file_open(&lfs2, &file, path, LFS2_O_WRONLY | LFS2_O_CREAT); + char wbuffer[1024]; + size = 7; + snprintf(wbuffer, size, "Hi %03d", i); + if ((lfs2_size_t)lfs2_file_size(&lfs2, &file) != size) { + lfs2_file_write(&lfs2, &file, wbuffer, size) => size; + } + lfs2_file_close(&lfs2, &file) => 0; + + char rbuffer[1024]; + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, rbuffer, size) => size; + assert(strcmp(rbuffer, wbuffer) == 0); + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_interspersed.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_interspersed.toml new file mode 100644 index 00000000000..c1f31d4396e --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_interspersed.toml @@ -0,0 +1,244 @@ + +[[case]] # interspersed file test +define.SIZE = [10, 100] +define.FILES = [4, 10, 26] +code = ''' + lfs2_file_t files[FILES]; + const char alphas[] = "abcdefghijklmnopqrstuvwxyz"; + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + for (int j = 0; j < FILES; j++) { + sprintf(path, "%c", alphas[j]); + lfs2_file_open(&lfs2, &files[j], path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + } + + for (int i = 0; i < SIZE; i++) { + for (int j = 0; j < FILES; j++) { + lfs2_file_write(&lfs2, &files[j], &alphas[j], 1) => 1; + } + } + + for (int j = 0; j < FILES; j++) { + lfs2_file_close(&lfs2, &files[j]); + } + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + for (int j = 0; j < FILES; j++) { + sprintf(path, "%c", alphas[j]); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == SIZE); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + for (int j = 0; j < FILES; j++) { + sprintf(path, "%c", alphas[j]); + lfs2_file_open(&lfs2, &files[j], path, LFS2_O_RDONLY) => 0; + } + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < FILES; j++) { + lfs2_file_read(&lfs2, &files[j], buffer, 1) => 1; + assert(buffer[0] == alphas[j]); + } + } + + for (int j = 0; j < FILES; j++) { + lfs2_file_close(&lfs2, &files[j]); + } + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # interspersed remove file test +define.SIZE = [10, 100] +define.FILES = [4, 10, 26] +code = ''' + const char alphas[] = "abcdefghijklmnopqrstuvwxyz"; + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + for (int j = 0; j < FILES; j++) { + sprintf(path, "%c", alphas[j]); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + for (int i = 0; i < SIZE; i++) { + lfs2_file_write(&lfs2, &file, &alphas[j], 1) => 1; + } + lfs2_file_close(&lfs2, &file); + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "zzz", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + for (int j = 0; j < FILES; j++) { + lfs2_file_write(&lfs2, &file, (const void*)"~", 1) => 1; + lfs2_file_sync(&lfs2, &file) => 0; + + sprintf(path, "%c", alphas[j]); + lfs2_remove(&lfs2, path) => 0; + } + lfs2_file_close(&lfs2, &file); + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "zzz") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == FILES); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "zzz", LFS2_O_RDONLY) => 0; + for (int i = 0; i < FILES; i++) { + lfs2_file_read(&lfs2, &file, buffer, 1) => 1; + assert(buffer[0] == '~'); + } + lfs2_file_close(&lfs2, &file); + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # remove inconveniently test +define.SIZE = [10, 100] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_t files[3]; + lfs2_file_open(&lfs2, &files[0], "e", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_open(&lfs2, &files[1], "f", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_open(&lfs2, &files[2], "g", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + + for (int i = 0; i < SIZE/2; i++) { + lfs2_file_write(&lfs2, &files[0], (const void*)"e", 1) => 1; + lfs2_file_write(&lfs2, &files[1], (const void*)"f", 1) => 1; + lfs2_file_write(&lfs2, &files[2], (const void*)"g", 1) => 1; + } + + lfs2_remove(&lfs2, "f") => 0; + + for (int i = 0; i < SIZE/2; i++) { + lfs2_file_write(&lfs2, &files[0], (const void*)"e", 1) => 1; + lfs2_file_write(&lfs2, &files[1], (const void*)"f", 1) => 1; + lfs2_file_write(&lfs2, &files[2], (const void*)"g", 1) => 1; + } + + lfs2_file_close(&lfs2, &files[0]); + lfs2_file_close(&lfs2, &files[1]); + lfs2_file_close(&lfs2, &files[2]); + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "e") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == SIZE); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "g") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == SIZE); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &files[0], "e", LFS2_O_RDONLY) => 0; + lfs2_file_open(&lfs2, &files[1], "g", LFS2_O_RDONLY) => 0; + for (int i = 0; i < SIZE; i++) { + lfs2_file_read(&lfs2, &files[0], buffer, 1) => 1; + assert(buffer[0] == 'e'); + lfs2_file_read(&lfs2, &files[1], buffer, 1) => 1; + assert(buffer[0] == 'g'); + } + lfs2_file_close(&lfs2, &files[0]); + lfs2_file_close(&lfs2, &files[1]); + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # reentrant interspersed file test +define.SIZE = [10, 100] +define.FILES = [4, 10, 26] +reentrant = true +code = ''' + lfs2_file_t files[FILES]; + const char alphas[] = "abcdefghijklmnopqrstuvwxyz"; + + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + for (int j = 0; j < FILES; j++) { + sprintf(path, "%c", alphas[j]); + lfs2_file_open(&lfs2, &files[j], path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + } + + for (int i = 0; i < SIZE; i++) { + for (int j = 0; j < FILES; j++) { + size = lfs2_file_size(&lfs2, &files[j]); + assert((int)size >= 0); + if ((int)size <= i) { + lfs2_file_write(&lfs2, &files[j], &alphas[j], 1) => 1; + lfs2_file_sync(&lfs2, &files[j]) => 0; + } + } + } + + for (int j = 0; j < FILES; j++) { + lfs2_file_close(&lfs2, &files[j]); + } + + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + for (int j = 0; j < FILES; j++) { + sprintf(path, "%c", alphas[j]); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, path) == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == SIZE); + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + for (int j = 0; j < FILES; j++) { + sprintf(path, "%c", alphas[j]); + lfs2_file_open(&lfs2, &files[j], path, LFS2_O_RDONLY) => 0; + } + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < FILES; j++) { + lfs2_file_read(&lfs2, &files[j], buffer, 1) => 1; + assert(buffer[0] == alphas[j]); + } + } + + for (int j = 0; j < FILES; j++) { + lfs2_file_close(&lfs2, &files[j]); + } + + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_move.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_move.toml new file mode 100644 index 00000000000..e2c63257859 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_move.toml @@ -0,0 +1,1815 @@ +[[case]] # move file +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + lfs2_file_write(&lfs2, &file, "hola\n", 5) => 5; + lfs2_file_write(&lfs2, &file, "bonjour\n", 8) => 8; + lfs2_file_write(&lfs2, &file, "ohayo\n", 6) => 6; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hello", "c/hello") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "b/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "c/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 5) => 5; + memcmp(buffer, "hola\n", 5) => 0; + lfs2_file_read(&lfs2, &file, buffer, 8) => 8; + memcmp(buffer, "bonjour\n", 8) => 0; + lfs2_file_read(&lfs2, &file, buffer, 6) => 6; + memcmp(buffer, "ohayo\n", 6) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "d/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # noop move, yes this is legal +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "hi") => 0; + lfs2_rename(&lfs2, "hi", "hi") => 0; + lfs2_mkdir(&lfs2, "hi/hi") => 0; + lfs2_rename(&lfs2, "hi/hi", "hi/hi") => 0; + lfs2_mkdir(&lfs2, "hi/hi/hi") => 0; + lfs2_rename(&lfs2, "hi/hi/hi", "hi/hi/hi") => 0; + lfs2_stat(&lfs2, "hi/hi/hi", &info) => 0; + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move file corrupt source +in = "lfs2.c" +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + lfs2_file_write(&lfs2, &file, "hola\n", 5) => 5; + lfs2_file_write(&lfs2, &file, "bonjour\n", 8) => 8; + lfs2_file_write(&lfs2, &file, "ohayo\n", 6) => 6; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hello", "c/hello") => 0; + lfs2_unmount(&lfs2) => 0; + + // corrupt the source + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_block_t block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + uint8_t bbuffer[LFS2_BLOCK_SIZE]; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + int off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "b/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "c/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 5) => 5; + memcmp(buffer, "hola\n", 5) => 0; + lfs2_file_read(&lfs2, &file, buffer, 8) => 8; + memcmp(buffer, "bonjour\n", 8) => 0; + lfs2_file_read(&lfs2, &file, buffer, 6) => 6; + memcmp(buffer, "ohayo\n", 6) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "d/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move file corrupt source and dest +in = "lfs2.c" +if = 'LFS2_PROG_SIZE <= 0x3fe' # only works with one crc per commit +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + lfs2_file_write(&lfs2, &file, "hola\n", 5) => 5; + lfs2_file_write(&lfs2, &file, "bonjour\n", 8) => 8; + lfs2_file_write(&lfs2, &file, "ohayo\n", 6) => 6; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hello", "c/hello") => 0; + lfs2_unmount(&lfs2) => 0; + + // corrupt the source + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_block_t block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + uint8_t bbuffer[LFS2_BLOCK_SIZE]; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + int off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + // corrupt the destination + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 5) => 5; + memcmp(buffer, "hola\n", 5) => 0; + lfs2_file_read(&lfs2, &file, buffer, 8) => 8; + memcmp(buffer, "bonjour\n", 8) => 0; + lfs2_file_read(&lfs2, &file, buffer, 6) => 6; + memcmp(buffer, "ohayo\n", 6) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "b/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "c/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "d/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move file after corrupt +in = "lfs2.c" +if = 'LFS2_PROG_SIZE <= 0x3fe' # only works with one crc per commit +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + lfs2_file_write(&lfs2, &file, "hola\n", 5) => 5; + lfs2_file_write(&lfs2, &file, "bonjour\n", 8) => 8; + lfs2_file_write(&lfs2, &file, "ohayo\n", 6) => 6; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hello", "c/hello") => 0; + lfs2_unmount(&lfs2) => 0; + + // corrupt the source + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_block_t block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + uint8_t bbuffer[LFS2_BLOCK_SIZE]; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + int off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + // corrupt the destination + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + // continue move + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hello", "c/hello") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "b/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "c/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 5) => 5; + memcmp(buffer, "hola\n", 5) => 0; + lfs2_file_read(&lfs2, &file, buffer, 8) => 8; + memcmp(buffer, "bonjour\n", 8) => 0; + lfs2_file_read(&lfs2, &file, buffer, 6) => 6; + memcmp(buffer, "ohayo\n", 6) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "d/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # simple reentrant move file +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + err = lfs2_mkdir(&lfs2, "a"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "b"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "c"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "d"); + assert(!err || err == LFS2_ERR_EXIST); + lfs2_unmount(&lfs2) => 0; + + while (true) { + lfs2_mount(&lfs2, &cfg) => 0; + // there should never exist _2_ hello files + int count = 0; + if (lfs2_stat(&lfs2, "a/hello", &info) == 0) { + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6 || info.size == 0); + count += 1; + } + if (lfs2_stat(&lfs2, "b/hello", &info) == 0) { + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6); + count += 1; + } + if (lfs2_stat(&lfs2, "c/hello", &info) == 0) { + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6); + count += 1; + } + if (lfs2_stat(&lfs2, "d/hello", &info) == 0) { + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6); + count += 1; + } + assert(count <= 1); + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + if (lfs2_stat(&lfs2, "a/hello", &info) == 0 && info.size > 0) { + lfs2_rename(&lfs2, "a/hello", "b/hello") => 0; + } else if (lfs2_stat(&lfs2, "b/hello", &info) == 0) { + lfs2_rename(&lfs2, "b/hello", "c/hello") => 0; + } else if (lfs2_stat(&lfs2, "c/hello", &info) == 0) { + lfs2_rename(&lfs2, "c/hello", "d/hello") => 0; + } else if (lfs2_stat(&lfs2, "d/hello", &info) == 0) { + // success + lfs2_unmount(&lfs2) => 0; + break; + } else { + // create file + lfs2_file_open(&lfs2, &file, "a/hello", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "hola\n", 5) => 5; + lfs2_file_write(&lfs2, &file, "bonjour\n", 8) => 8; + lfs2_file_write(&lfs2, &file, "ohayo\n", 6) => 6; + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_unmount(&lfs2) => 0; + } + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "d") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hello") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 5+8+6); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "b/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "c/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "d/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 5) => 5; + memcmp(buffer, "hola\n", 5) => 0; + lfs2_file_read(&lfs2, &file, buffer, 8) => 8; + memcmp(buffer, "bonjour\n", 8) => 0; + lfs2_file_read(&lfs2, &file, buffer, 6) => 6; + memcmp(buffer, "ohayo\n", 6) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move dir +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_mkdir(&lfs2, "a/hi") => 0; + lfs2_mkdir(&lfs2, "a/hi/hola") => 0; + lfs2_mkdir(&lfs2, "a/hi/bonjour") => 0; + lfs2_mkdir(&lfs2, "a/hi/ohayo") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hi", "c/hi") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "a/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "b/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "c/hi") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "bonjour") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hola") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "ohayo") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "d/hi") => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move dir corrupt source +in = "lfs2.c" +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_mkdir(&lfs2, "a/hi") => 0; + lfs2_mkdir(&lfs2, "a/hi/hola") => 0; + lfs2_mkdir(&lfs2, "a/hi/bonjour") => 0; + lfs2_mkdir(&lfs2, "a/hi/ohayo") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hi", "c/hi") => 0; + lfs2_unmount(&lfs2) => 0; + + // corrupt the source + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_block_t block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + uint8_t bbuffer[LFS2_BLOCK_SIZE]; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + int off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "a/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "b/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "c/hi") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "bonjour") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hola") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "ohayo") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "d/hi") => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move dir corrupt source and dest +in = "lfs2.c" +if = 'LFS2_PROG_SIZE <= 0x3fe' # only works with one crc per commit +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_mkdir(&lfs2, "a/hi") => 0; + lfs2_mkdir(&lfs2, "a/hi/hola") => 0; + lfs2_mkdir(&lfs2, "a/hi/bonjour") => 0; + lfs2_mkdir(&lfs2, "a/hi/ohayo") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hi", "c/hi") => 0; + lfs2_unmount(&lfs2) => 0; + + // corrupt the source + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_block_t block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + uint8_t bbuffer[LFS2_BLOCK_SIZE]; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + int off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + // corrupt the destination + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "a/hi") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "bonjour") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hola") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "ohayo") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "b/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "c/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "d/hi") => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move dir after corrupt +in = "lfs2.c" +if = 'LFS2_PROG_SIZE <= 0x3fe' # only works with one crc per commit +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_mkdir(&lfs2, "a/hi") => 0; + lfs2_mkdir(&lfs2, "a/hi/hola") => 0; + lfs2_mkdir(&lfs2, "a/hi/bonjour") => 0; + lfs2_mkdir(&lfs2, "a/hi/ohayo") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hi", "c/hi") => 0; + lfs2_unmount(&lfs2) => 0; + + // corrupt the source + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_block_t block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + uint8_t bbuffer[LFS2_BLOCK_SIZE]; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + int off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + // corrupt the destination + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + // continue move + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hi", "c/hi") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "c") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "a/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "b/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "c/hi") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "bonjour") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hola") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "ohayo") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "d/hi") => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # simple reentrant move dir +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + err = lfs2_mkdir(&lfs2, "a"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "b"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "c"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "d"); + assert(!err || err == LFS2_ERR_EXIST); + lfs2_unmount(&lfs2) => 0; + + while (true) { + lfs2_mount(&lfs2, &cfg) => 0; + // there should never exist _2_ hi directories + int count = 0; + if (lfs2_stat(&lfs2, "a/hi", &info) == 0) { + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + count += 1; + } + if (lfs2_stat(&lfs2, "b/hi", &info) == 0) { + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + count += 1; + } + if (lfs2_stat(&lfs2, "c/hi", &info) == 0) { + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + count += 1; + } + if (lfs2_stat(&lfs2, "d/hi", &info) == 0) { + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + count += 1; + } + assert(count <= 1); + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + if (lfs2_stat(&lfs2, "a/hi", &info) == 0) { + lfs2_rename(&lfs2, "a/hi", "b/hi") => 0; + } else if (lfs2_stat(&lfs2, "b/hi", &info) == 0) { + lfs2_rename(&lfs2, "b/hi", "c/hi") => 0; + } else if (lfs2_stat(&lfs2, "c/hi", &info) == 0) { + lfs2_rename(&lfs2, "c/hi", "d/hi") => 0; + } else if (lfs2_stat(&lfs2, "d/hi", &info) == 0) { + lfs2_unmount(&lfs2) => 0; + break; // success + } else { + // create dir and rename for atomicity + err = lfs2_mkdir(&lfs2, "temp"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "temp/hola"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "temp/bonjour"); + assert(!err || err == LFS2_ERR_EXIST); + err = lfs2_mkdir(&lfs2, "temp/ohayo"); + assert(!err || err == LFS2_ERR_EXIST); + lfs2_rename(&lfs2, "temp", "a/hi") => 0; + } + lfs2_unmount(&lfs2) => 0; + } + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "a") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_dir_open(&lfs2, &dir, "d") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hi") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "a/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "b/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "c/hi") => LFS2_ERR_NOENT; + lfs2_dir_open(&lfs2, &dir, "d/hi") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "bonjour") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "hola") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "ohayo") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move state stealing +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "a") => 0; + lfs2_mkdir(&lfs2, "b") => 0; + lfs2_mkdir(&lfs2, "c") => 0; + lfs2_mkdir(&lfs2, "d") => 0; + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + lfs2_file_write(&lfs2, &file, "hola\n", 5) => 5; + lfs2_file_write(&lfs2, &file, "bonjour\n", 8) => 8; + lfs2_file_write(&lfs2, &file, "ohayo\n", 6) => 6; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "a/hello", "b/hello") => 0; + lfs2_unmount(&lfs2) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "b/hello", "c/hello") => 0; + lfs2_unmount(&lfs2) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_rename(&lfs2, "c/hello", "d/hello") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "b/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "c/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "d/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 5) => 5; + memcmp(buffer, "hola\n", 5) => 0; + lfs2_file_read(&lfs2, &file, buffer, 8) => 8; + memcmp(buffer, "bonjour\n", 8) => 0; + lfs2_file_read(&lfs2, &file, buffer, 6) => 6; + memcmp(buffer, "ohayo\n", 6) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_remove(&lfs2, "b") => 0; + lfs2_remove(&lfs2, "c") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "a", &info) => 0; + lfs2_stat(&lfs2, "b", &info) => LFS2_ERR_NOENT; + lfs2_stat(&lfs2, "c", &info) => LFS2_ERR_NOENT; + lfs2_stat(&lfs2, "d", &info) => 0; + lfs2_file_open(&lfs2, &file, "a/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "b/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "c/hello", LFS2_O_RDONLY) => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "d/hello", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 5) => 5; + memcmp(buffer, "hola\n", 5) => 0; + lfs2_file_read(&lfs2, &file, buffer, 8) => 8; + memcmp(buffer, "bonjour\n", 8) => 0; + lfs2_file_read(&lfs2, &file, buffer, 6) => 6; + memcmp(buffer, "ohayo\n", 6) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +# Other specific corner cases +[[case]] # create + delete in same commit with neighbors +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // littlefs keeps files sorted, so we know the order these will be in + lfs2_file_open(&lfs2, &file, "/1.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/0.before", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.1", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/2.in_between", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.2", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/4.after", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.3", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_t files[3]; + lfs2_file_open(&lfs2, &files[0], "0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[1], "2.in_between", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[2], "4.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_write(&lfs2, &files[0], "test.4", 7) => 7; + lfs2_file_write(&lfs2, &files[1], "test.5", 7) => 7; + lfs2_file_write(&lfs2, &files[2], "test.6", 7) => 7; + + // rename file while everything is open, this triggers both + // a create and delete simultaneously + lfs2_rename(&lfs2, "/1.move_me", "/3.move_me") => 0; + + lfs2_file_close(&lfs2, &files[0]) => 0; + lfs2_file_close(&lfs2, &files[1]) => 0; + lfs2_file_close(&lfs2, &files[2]) => 0; + + // check that nothing was corrupted + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.in_between") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "3.move_me") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "4.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.4") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/2.in_between", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.5") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/4.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.6") == 0); + lfs2_file_close(&lfs2, &file) => 0; + + // now move back + lfs2_file_open(&lfs2, &files[0], "0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[1], "2.in_between", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[2], "4.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_write(&lfs2, &files[0], "test.7", 7) => 7; + lfs2_file_write(&lfs2, &files[1], "test.8", 7) => 7; + lfs2_file_write(&lfs2, &files[2], "test.9", 7) => 7; + + // rename file while everything is open, this triggers both + // a create and delete simultaneously + lfs2_rename(&lfs2, "/3.move_me", "/1.move_me") => 0; + + lfs2_file_close(&lfs2, &files[0]) => 0; + lfs2_file_close(&lfs2, &files[1]) => 0; + lfs2_file_close(&lfs2, &files[2]) => 0; + + // and check that nothing was corrupted again + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "1.move_me") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.in_between") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "4.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.7") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/2.in_between", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.8") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/4.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.9") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +# Other specific corner cases +[[case]] # create + delete + delete in same commit with neighbors +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // littlefs keeps files sorted, so we know the order these will be in + lfs2_file_open(&lfs2, &file, "/1.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/3.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "remove me", + sizeof("remove me")) => sizeof("remove me"); + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/0.before", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.1", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/2.in_between", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.2", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/4.after", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.3", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_t files[3]; + lfs2_file_open(&lfs2, &files[0], "0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[1], "2.in_between", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[2], "4.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_write(&lfs2, &files[0], "test.4", 7) => 7; + lfs2_file_write(&lfs2, &files[1], "test.5", 7) => 7; + lfs2_file_write(&lfs2, &files[2], "test.6", 7) => 7; + + // rename file while everything is open, this triggers both + // a create and delete simultaneously + lfs2_rename(&lfs2, "/1.move_me", "/3.move_me") => 0; + + lfs2_file_close(&lfs2, &files[0]) => 0; + lfs2_file_close(&lfs2, &files[1]) => 0; + lfs2_file_close(&lfs2, &files[2]) => 0; + + // check that nothing was corrupted + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.in_between") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "3.move_me") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "4.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.4") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/2.in_between", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.5") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/4.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.6") == 0); + lfs2_file_close(&lfs2, &file) => 0; + + // now move back + lfs2_file_open(&lfs2, &file, "/1.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "remove me", + sizeof("remove me")) => sizeof("remove me"); + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &files[0], "0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[1], "2.in_between", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[2], "4.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_write(&lfs2, &files[0], "test.7", 7) => 7; + lfs2_file_write(&lfs2, &files[1], "test.8", 7) => 7; + lfs2_file_write(&lfs2, &files[2], "test.9", 7) => 7; + + // rename file while everything is open, this triggers both + // a create and delete simultaneously + lfs2_rename(&lfs2, "/3.move_me", "/1.move_me") => 0; + + lfs2_file_close(&lfs2, &files[0]) => 0; + lfs2_file_close(&lfs2, &files[1]) => 0; + lfs2_file_close(&lfs2, &files[2]) => 0; + + // and check that nothing was corrupted again + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "1.move_me") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.in_between") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "4.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.7") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/2.in_between", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.8") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/4.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.9") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # create + delete in different dirs with neighbors +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + // littlefs keeps files sorted, so we know the order these will be in + lfs2_mkdir(&lfs2, "/dir.1") => 0; + lfs2_mkdir(&lfs2, "/dir.2") => 0; + lfs2_file_open(&lfs2, &file, "/dir.1/1.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.2/1.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "remove me", + sizeof("remove me")) => sizeof("remove me"); + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/dir.1/0.before", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.1", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.1/2.after", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.2", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/dir.2/0.before", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.3", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.2/2.after", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.4", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_t files[4]; + lfs2_file_open(&lfs2, &files[0], "/dir.1/0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[1], "/dir.1/2.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[2], "/dir.2/0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[3], "/dir.2/2.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_write(&lfs2, &files[0], "test.5", 7) => 7; + lfs2_file_write(&lfs2, &files[1], "test.6", 7) => 7; + lfs2_file_write(&lfs2, &files[2], "test.7", 7) => 7; + lfs2_file_write(&lfs2, &files[3], "test.8", 7) => 7; + + // rename file while everything is open, this triggers both + // a create and delete as it overwrites the destination file + lfs2_rename(&lfs2, "/dir.1/1.move_me", "/dir.2/1.move_me") => 0; + + lfs2_file_close(&lfs2, &files[0]) => 0; + lfs2_file_close(&lfs2, &files[1]) => 0; + lfs2_file_close(&lfs2, &files[2]) => 0; + lfs2_file_close(&lfs2, &files[3]) => 0; + + // check that nothing was corrupted + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "dir.1") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "dir.2") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "/dir.1") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "/dir.2") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "1.move_me") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "/dir.1/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.5") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.1/2.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.6") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.2/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.7") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.2/2.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.8") == 0); + lfs2_file_close(&lfs2, &file) => 0; + + // now move back + lfs2_file_open(&lfs2, &file, "/dir.1/1.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "remove me", + sizeof("remove me")) => sizeof("remove me"); + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &files[0], "/dir.1/0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[1], "/dir.1/2.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[2], "/dir.2/0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[3], "/dir.2/2.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_write(&lfs2, &files[0], "test.9", 7) => 7; + lfs2_file_write(&lfs2, &files[1], "test.a", 7) => 7; + lfs2_file_write(&lfs2, &files[2], "test.b", 7) => 7; + lfs2_file_write(&lfs2, &files[3], "test.c", 7) => 7; + + // rename file while everything is open, this triggers both + // a create and delete simultaneously + lfs2_rename(&lfs2, "/dir.2/1.move_me", "/dir.1/1.move_me") => 0; + + lfs2_file_close(&lfs2, &files[0]) => 0; + lfs2_file_close(&lfs2, &files[1]) => 0; + lfs2_file_close(&lfs2, &files[2]) => 0; + lfs2_file_close(&lfs2, &files[3]) => 0; + + // and check that nothing was corrupted again + lfs2_dir_open(&lfs2, &dir, "/") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "dir.1") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "dir.2") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "/dir.1") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "1.move_me") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 0); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "/dir.2") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "/dir.1/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.9") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.1/2.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.a") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.2/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.b") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/dir.2/2.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.c") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move fix in relocation +in = "lfs2.c" +define.RELOCATIONS = 'range(0x3+1)' +define.LFS2_ERASE_CYCLES = 0xffffffff +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + lfs2_mkdir(&lfs2, "/parent") => 0; + lfs2_mkdir(&lfs2, "/parent/child") => 0; + + lfs2_file_open(&lfs2, &file, "/parent/1.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "move me", + sizeof("move me")) => sizeof("move me"); + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/parent/0.before", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.1", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/2.after", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.2", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/child/0.before", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.3", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/child/2.after", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.4", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_t files[4]; + lfs2_file_open(&lfs2, &files[0], "/parent/0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[1], "/parent/2.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[2], "/parent/child/0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[3], "/parent/child/2.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_write(&lfs2, &files[0], "test.5", 7) => 7; + lfs2_file_write(&lfs2, &files[1], "test.6", 7) => 7; + lfs2_file_write(&lfs2, &files[2], "test.7", 7) => 7; + lfs2_file_write(&lfs2, &files[3], "test.8", 7) => 7; + + // force specific directories to relocate + if (RELOCATIONS & 0x1) { + lfs2_dir_open(&lfs2, &dir, "/parent"); + lfs2_testbd_setwear(&cfg, dir.m.pair[0], 0xffffffff) => 0; + lfs2_testbd_setwear(&cfg, dir.m.pair[1], 0xffffffff) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + } + if (RELOCATIONS & 0x2) { + lfs2_dir_open(&lfs2, &dir, "/parent/child"); + lfs2_testbd_setwear(&cfg, dir.m.pair[0], 0xffffffff) => 0; + lfs2_testbd_setwear(&cfg, dir.m.pair[1], 0xffffffff) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + } + + // ok, now we move the file, this creates a move that needs to be + // fixed, possibly in a metadata-pair that needs to be relocated + // + // the worst case is if we need to relocate and we need to implicit + // fix the move in our parent before it falls out of date + lfs2_rename(&lfs2, "/parent/1.move_me", "/parent/child/1.move_me") => 0; + + lfs2_file_close(&lfs2, &files[0]) => 0; + lfs2_file_close(&lfs2, &files[1]) => 0; + lfs2_file_close(&lfs2, &files[2]) => 0; + lfs2_file_close(&lfs2, &files[3]) => 0; + + // check that nothing was corrupted + lfs2_dir_open(&lfs2, &dir, "/parent") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "child") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "/parent/child") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "1.move_me") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == sizeof("move me")); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "/parent/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.5") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/2.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.6") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/child/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.7") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/child/2.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.8") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # move fix in relocation with predecessor +in = "lfs2.c" +define.RELOCATIONS = 'range(0x7+1)' +define.LFS2_ERASE_CYCLES = 0xffffffff +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + lfs2_mkdir(&lfs2, "/parent") => 0; + lfs2_mkdir(&lfs2, "/parent/child") => 0; + lfs2_mkdir(&lfs2, "/parent/sibling") => 0; + + lfs2_file_open(&lfs2, &file, "/parent/sibling/1.move_me", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "move me", + sizeof("move me")) => sizeof("move me"); + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "/parent/sibling/0.before", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.1", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/sibling/2.after", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.2", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/child/0.before", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.3", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/child/2.after", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_write(&lfs2, &file, "test.4", 7) => 7; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_t files[4]; + lfs2_file_open(&lfs2, &files[0], "/parent/sibling/0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[1], "/parent/sibling/2.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[2], "/parent/child/0.before", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_open(&lfs2, &files[3], "/parent/child/2.after", + LFS2_O_WRONLY | LFS2_O_TRUNC) => 0; + lfs2_file_write(&lfs2, &files[0], "test.5", 7) => 7; + lfs2_file_write(&lfs2, &files[1], "test.6", 7) => 7; + lfs2_file_write(&lfs2, &files[2], "test.7", 7) => 7; + lfs2_file_write(&lfs2, &files[3], "test.8", 7) => 7; + + // force specific directories to relocate + if (RELOCATIONS & 0x1) { + lfs2_dir_open(&lfs2, &dir, "/parent"); + lfs2_testbd_setwear(&cfg, dir.m.pair[0], 0xffffffff) => 0; + lfs2_testbd_setwear(&cfg, dir.m.pair[1], 0xffffffff) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + } + if (RELOCATIONS & 0x2) { + lfs2_dir_open(&lfs2, &dir, "/parent/sibling"); + lfs2_testbd_setwear(&cfg, dir.m.pair[0], 0xffffffff) => 0; + lfs2_testbd_setwear(&cfg, dir.m.pair[1], 0xffffffff) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + } + if (RELOCATIONS & 0x4) { + lfs2_dir_open(&lfs2, &dir, "/parent/child"); + lfs2_testbd_setwear(&cfg, dir.m.pair[0], 0xffffffff) => 0; + lfs2_testbd_setwear(&cfg, dir.m.pair[1], 0xffffffff) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + } + + // ok, now we move the file, this creates a move that needs to be + // fixed, possibly in a metadata-pair that needs to be relocated + // + // and now relocations can force us to need to fix our move in either + // the parent or child before things break + lfs2_rename(&lfs2, + "/parent/sibling/1.move_me", + "/parent/child/1.move_me") => 0; + + lfs2_file_close(&lfs2, &files[0]) => 0; + lfs2_file_close(&lfs2, &files[1]) => 0; + lfs2_file_close(&lfs2, &files[2]) => 0; + lfs2_file_close(&lfs2, &files[3]) => 0; + + // check that nothing was corrupted + lfs2_dir_open(&lfs2, &dir, "/parent") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "child") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "sibling") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "/parent/sibling") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_dir_open(&lfs2, &dir, "/parent/child") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, ".") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "..") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "0.before") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "1.move_me") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == sizeof("move me")); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + assert(strcmp(info.name, "2.after") == 0); + assert(info.type == LFS2_TYPE_REG); + assert(info.size == 7); + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + lfs2_file_open(&lfs2, &file, "/parent/sibling/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.5") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/sibling/2.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.6") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/child/0.before", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.7") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_file_open(&lfs2, &file, "/parent/child/2.after", LFS2_O_RDONLY) => 0; + lfs2_file_read(&lfs2, &file, buffer, 7) => 7; + assert(strcmp((char*)buffer, "test.8") == 0); + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_orphans.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_orphans.toml new file mode 100644 index 00000000000..5abfc229ce5 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_orphans.toml @@ -0,0 +1,120 @@ +[[case]] # orphan test +in = "lfs2.c" +if = 'LFS2_PROG_SIZE <= 0x3fe' # only works with one crc per commit +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "parent") => 0; + lfs2_mkdir(&lfs2, "parent/orphan") => 0; + lfs2_mkdir(&lfs2, "parent/child") => 0; + lfs2_remove(&lfs2, "parent/orphan") => 0; + lfs2_unmount(&lfs2) => 0; + + // corrupt the child's most recent commit, this should be the update + // to the linked-list entry, which should orphan the orphan. Note this + // makes a lot of assumptions about the remove operation. + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "parent/child") => 0; + lfs2_block_t block = dir.m.pair[0]; + lfs2_dir_close(&lfs2, &dir) => 0; + lfs2_unmount(&lfs2) => 0; + uint8_t bbuffer[LFS2_BLOCK_SIZE]; + cfg.read(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + int off = LFS2_BLOCK_SIZE-1; + while (off >= 0 && bbuffer[off] == LFS2_ERASE_VALUE) { + off -= 1; + } + memset(&bbuffer[off-3], LFS2_BLOCK_SIZE, 3); + cfg.erase(&cfg, block) => 0; + cfg.prog(&cfg, block, 0, bbuffer, LFS2_BLOCK_SIZE) => 0; + cfg.sync(&cfg) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "parent/orphan", &info) => LFS2_ERR_NOENT; + lfs2_stat(&lfs2, "parent/child", &info) => 0; + lfs2_fs_size(&lfs2) => 8; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "parent/orphan", &info) => LFS2_ERR_NOENT; + lfs2_stat(&lfs2, "parent/child", &info) => 0; + lfs2_fs_size(&lfs2) => 8; + // this mkdir should both create a dir and deorphan, so size + // should be unchanged + lfs2_mkdir(&lfs2, "parent/otherchild") => 0; + lfs2_stat(&lfs2, "parent/orphan", &info) => LFS2_ERR_NOENT; + lfs2_stat(&lfs2, "parent/child", &info) => 0; + lfs2_stat(&lfs2, "parent/otherchild", &info) => 0; + lfs2_fs_size(&lfs2) => 8; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "parent/orphan", &info) => LFS2_ERR_NOENT; + lfs2_stat(&lfs2, "parent/child", &info) => 0; + lfs2_stat(&lfs2, "parent/otherchild", &info) => 0; + lfs2_fs_size(&lfs2) => 8; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # reentrant testing for orphans, basically just spam mkdir/remove +reentrant = true +# TODO fix this case, caused by non-DAG trees +if = '!(DEPTH == 3 && LFS2_CACHE_SIZE != 64)' +define = [ + {FILES=6, DEPTH=1, CYCLES=20}, + {FILES=26, DEPTH=1, CYCLES=20}, + {FILES=3, DEPTH=3, CYCLES=20}, +] +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + srand(1); + const char alpha[] = "abcdefghijklmnopqrstuvwxyz"; + for (int i = 0; i < CYCLES; i++) { + // create random path + char full_path[256]; + for (int d = 0; d < DEPTH; d++) { + sprintf(&full_path[2*d], "/%c", alpha[rand() % FILES]); + } + + // if it does not exist, we create it, else we destroy + int res = lfs2_stat(&lfs2, full_path, &info); + if (res == LFS2_ERR_NOENT) { + // create each directory in turn, ignore if dir already exists + for (int d = 0; d < DEPTH; d++) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + err = lfs2_mkdir(&lfs2, path); + assert(!err || err == LFS2_ERR_EXIST); + } + + for (int d = 0; d < DEPTH; d++) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + lfs2_stat(&lfs2, path, &info) => 0; + assert(strcmp(info.name, &path[2*d+1]) == 0); + assert(info.type == LFS2_TYPE_DIR); + } + } else { + // is valid dir? + assert(strcmp(info.name, &full_path[2*(DEPTH-1)+1]) == 0); + assert(info.type == LFS2_TYPE_DIR); + + // try to delete path in reverse order, ignore if dir is not empty + for (int d = DEPTH-1; d >= 0; d--) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + err = lfs2_remove(&lfs2, path); + assert(!err || err == LFS2_ERR_NOTEMPTY); + } + + lfs2_stat(&lfs2, full_path, &info) => LFS2_ERR_NOENT; + } + } + lfs2_unmount(&lfs2) => 0; +''' + diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_paths.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_paths.toml new file mode 100644 index 00000000000..f6b7f179330 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_paths.toml @@ -0,0 +1,293 @@ + +[[case]] # simple path test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "tea") => 0; + lfs2_mkdir(&lfs2, "tea/hottea") => 0; + lfs2_mkdir(&lfs2, "tea/warmtea") => 0; + lfs2_mkdir(&lfs2, "tea/coldtea") => 0; + + lfs2_stat(&lfs2, "tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "/tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + + lfs2_mkdir(&lfs2, "/milk") => 0; + lfs2_stat(&lfs2, "/milk", &info) => 0; + assert(strcmp(info.name, "milk") == 0); + lfs2_stat(&lfs2, "milk", &info) => 0; + assert(strcmp(info.name, "milk") == 0); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # redundant slashes +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "tea") => 0; + lfs2_mkdir(&lfs2, "tea/hottea") => 0; + lfs2_mkdir(&lfs2, "tea/warmtea") => 0; + lfs2_mkdir(&lfs2, "tea/coldtea") => 0; + + lfs2_stat(&lfs2, "/tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "//tea//hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "///tea///hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + + lfs2_mkdir(&lfs2, "////milk") => 0; + lfs2_stat(&lfs2, "////milk", &info) => 0; + assert(strcmp(info.name, "milk") == 0); + lfs2_stat(&lfs2, "milk", &info) => 0; + assert(strcmp(info.name, "milk") == 0); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # dot path test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "tea") => 0; + lfs2_mkdir(&lfs2, "tea/hottea") => 0; + lfs2_mkdir(&lfs2, "tea/warmtea") => 0; + lfs2_mkdir(&lfs2, "tea/coldtea") => 0; + + lfs2_stat(&lfs2, "./tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "/./tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "/././tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "/./tea/./hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + + lfs2_mkdir(&lfs2, "/./milk") => 0; + lfs2_stat(&lfs2, "/./milk", &info) => 0; + assert(strcmp(info.name, "milk") == 0); + lfs2_stat(&lfs2, "milk", &info) => 0; + assert(strcmp(info.name, "milk") == 0); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # dot dot path test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "tea") => 0; + lfs2_mkdir(&lfs2, "tea/hottea") => 0; + lfs2_mkdir(&lfs2, "tea/warmtea") => 0; + lfs2_mkdir(&lfs2, "tea/coldtea") => 0; + lfs2_mkdir(&lfs2, "coffee") => 0; + lfs2_mkdir(&lfs2, "coffee/hotcoffee") => 0; + lfs2_mkdir(&lfs2, "coffee/warmcoffee") => 0; + lfs2_mkdir(&lfs2, "coffee/coldcoffee") => 0; + + lfs2_stat(&lfs2, "coffee/../tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "tea/coldtea/../hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "coffee/coldcoffee/../../tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "coffee/../coffee/../tea/hottea", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + + lfs2_mkdir(&lfs2, "coffee/../milk") => 0; + lfs2_stat(&lfs2, "coffee/../milk", &info) => 0; + strcmp(info.name, "milk") => 0; + lfs2_stat(&lfs2, "milk", &info) => 0; + strcmp(info.name, "milk") => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # trailing dot path test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "tea") => 0; + lfs2_mkdir(&lfs2, "tea/hottea") => 0; + lfs2_mkdir(&lfs2, "tea/warmtea") => 0; + lfs2_mkdir(&lfs2, "tea/coldtea") => 0; + + lfs2_stat(&lfs2, "tea/hottea/", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "tea/hottea/.", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "tea/hottea/./.", &info) => 0; + assert(strcmp(info.name, "hottea") == 0); + lfs2_stat(&lfs2, "tea/hottea/..", &info) => 0; + assert(strcmp(info.name, "tea") == 0); + lfs2_stat(&lfs2, "tea/hottea/../.", &info) => 0; + assert(strcmp(info.name, "tea") == 0); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # leading dot path test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, ".milk") => 0; + lfs2_stat(&lfs2, ".milk", &info) => 0; + strcmp(info.name, ".milk") => 0; + lfs2_stat(&lfs2, "tea/.././.milk", &info) => 0; + strcmp(info.name, ".milk") => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # root dot dot path test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "tea") => 0; + lfs2_mkdir(&lfs2, "tea/hottea") => 0; + lfs2_mkdir(&lfs2, "tea/warmtea") => 0; + lfs2_mkdir(&lfs2, "tea/coldtea") => 0; + lfs2_mkdir(&lfs2, "coffee") => 0; + lfs2_mkdir(&lfs2, "coffee/hotcoffee") => 0; + lfs2_mkdir(&lfs2, "coffee/warmcoffee") => 0; + lfs2_mkdir(&lfs2, "coffee/coldcoffee") => 0; + + lfs2_stat(&lfs2, "coffee/../../../../../../tea/hottea", &info) => 0; + strcmp(info.name, "hottea") => 0; + + lfs2_mkdir(&lfs2, "coffee/../../../../../../milk") => 0; + lfs2_stat(&lfs2, "coffee/../../../../../../milk", &info) => 0; + strcmp(info.name, "milk") => 0; + lfs2_stat(&lfs2, "milk", &info) => 0; + strcmp(info.name, "milk") => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # invalid path tests +code = ''' + lfs2_format(&lfs2, &cfg); + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "dirt", &info) => LFS2_ERR_NOENT; + lfs2_stat(&lfs2, "dirt/ground", &info) => LFS2_ERR_NOENT; + lfs2_stat(&lfs2, "dirt/ground/earth", &info) => LFS2_ERR_NOENT; + + lfs2_remove(&lfs2, "dirt") => LFS2_ERR_NOENT; + lfs2_remove(&lfs2, "dirt/ground") => LFS2_ERR_NOENT; + lfs2_remove(&lfs2, "dirt/ground/earth") => LFS2_ERR_NOENT; + + lfs2_mkdir(&lfs2, "dirt/ground") => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "dirt/ground", LFS2_O_WRONLY | LFS2_O_CREAT) + => LFS2_ERR_NOENT; + lfs2_mkdir(&lfs2, "dirt/ground/earth") => LFS2_ERR_NOENT; + lfs2_file_open(&lfs2, &file, "dirt/ground/earth", LFS2_O_WRONLY | LFS2_O_CREAT) + => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # root operations +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "/", &info) => 0; + assert(strcmp(info.name, "/") == 0); + assert(info.type == LFS2_TYPE_DIR); + + lfs2_mkdir(&lfs2, "/") => LFS2_ERR_EXIST; + lfs2_file_open(&lfs2, &file, "/", LFS2_O_WRONLY | LFS2_O_CREAT) + => LFS2_ERR_ISDIR; + + lfs2_remove(&lfs2, "/") => LFS2_ERR_INVAL; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # root representations +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "/", &info) => 0; + assert(strcmp(info.name, "/") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_stat(&lfs2, "", &info) => 0; + assert(strcmp(info.name, "/") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_stat(&lfs2, ".", &info) => 0; + assert(strcmp(info.name, "/") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_stat(&lfs2, "..", &info) => 0; + assert(strcmp(info.name, "/") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_stat(&lfs2, "//", &info) => 0; + assert(strcmp(info.name, "/") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_stat(&lfs2, "./", &info) => 0; + assert(strcmp(info.name, "/") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # superblock conflict test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "littlefs", &info) => LFS2_ERR_NOENT; + lfs2_remove(&lfs2, "littlefs") => LFS2_ERR_NOENT; + + lfs2_mkdir(&lfs2, "littlefs") => 0; + lfs2_stat(&lfs2, "littlefs", &info) => 0; + assert(strcmp(info.name, "littlefs") == 0); + assert(info.type == LFS2_TYPE_DIR); + lfs2_remove(&lfs2, "littlefs") => 0; + lfs2_stat(&lfs2, "littlefs", &info) => LFS2_ERR_NOENT; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # max path test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "coffee") => 0; + lfs2_mkdir(&lfs2, "coffee/hotcoffee") => 0; + lfs2_mkdir(&lfs2, "coffee/warmcoffee") => 0; + lfs2_mkdir(&lfs2, "coffee/coldcoffee") => 0; + + memset(path, 'w', LFS2_NAME_MAX+1); + path[LFS2_NAME_MAX+1] = '\0'; + lfs2_mkdir(&lfs2, path) => LFS2_ERR_NAMETOOLONG; + lfs2_file_open(&lfs2, &file, path, LFS2_O_WRONLY | LFS2_O_CREAT) + => LFS2_ERR_NAMETOOLONG; + + memcpy(path, "coffee/", strlen("coffee/")); + memset(path+strlen("coffee/"), 'w', LFS2_NAME_MAX+1); + path[strlen("coffee/")+LFS2_NAME_MAX+1] = '\0'; + lfs2_mkdir(&lfs2, path) => LFS2_ERR_NAMETOOLONG; + lfs2_file_open(&lfs2, &file, path, LFS2_O_WRONLY | LFS2_O_CREAT) + => LFS2_ERR_NAMETOOLONG; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # really big path test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_mkdir(&lfs2, "coffee") => 0; + lfs2_mkdir(&lfs2, "coffee/hotcoffee") => 0; + lfs2_mkdir(&lfs2, "coffee/warmcoffee") => 0; + lfs2_mkdir(&lfs2, "coffee/coldcoffee") => 0; + + memset(path, 'w', LFS2_NAME_MAX); + path[LFS2_NAME_MAX] = '\0'; + lfs2_mkdir(&lfs2, path) => 0; + lfs2_remove(&lfs2, path) => 0; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_remove(&lfs2, path) => 0; + + memcpy(path, "coffee/", strlen("coffee/")); + memset(path+strlen("coffee/"), 'w', LFS2_NAME_MAX); + path[strlen("coffee/")+LFS2_NAME_MAX] = '\0'; + lfs2_mkdir(&lfs2, path) => 0; + lfs2_remove(&lfs2, path) => 0; + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_remove(&lfs2, path) => 0; + lfs2_unmount(&lfs2) => 0; +''' + diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_relocations.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_relocations.toml new file mode 100644 index 00000000000..60bfa1dff14 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_relocations.toml @@ -0,0 +1,305 @@ +# specific corner cases worth explicitly testing for +[[case]] # dangling split dir test +define.ITERATIONS = 20 +define.COUNT = 10 +define.LFS2_BLOCK_CYCLES = [8, 1] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + // fill up filesystem so only ~16 blocks are left + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "padding", LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + memset(buffer, 0, 512); + while (LFS2_BLOCK_COUNT - lfs2_fs_size(&lfs2) > 16) { + lfs2_file_write(&lfs2, &file, buffer, 512) => 512; + } + lfs2_file_close(&lfs2, &file) => 0; + // make a child dir to use in bounded space + lfs2_mkdir(&lfs2, "child") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int j = 0; j < ITERATIONS; j++) { + for (int i = 0; i < COUNT; i++) { + sprintf(path, "child/test%03d_loooooooooooooooooong_name", i); + lfs2_file_open(&lfs2, &file, path, LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + lfs2_file_close(&lfs2, &file) => 0; + } + + lfs2_dir_open(&lfs2, &dir, "child") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + for (int i = 0; i < COUNT; i++) { + sprintf(path, "test%03d_loooooooooooooooooong_name", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + strcmp(info.name, path) => 0; + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + if (j == ITERATIONS-1) { + break; + } + + for (int i = 0; i < COUNT; i++) { + sprintf(path, "child/test%03d_loooooooooooooooooong_name", i); + lfs2_remove(&lfs2, path) => 0; + } + } + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_dir_open(&lfs2, &dir, "child") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + for (int i = 0; i < COUNT; i++) { + sprintf(path, "test%03d_loooooooooooooooooong_name", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + strcmp(info.name, path) => 0; + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + for (int i = 0; i < COUNT; i++) { + sprintf(path, "child/test%03d_loooooooooooooooooong_name", i); + lfs2_remove(&lfs2, path) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # outdated head test +define.ITERATIONS = 20 +define.COUNT = 10 +define.LFS2_BLOCK_CYCLES = [8, 1] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + // fill up filesystem so only ~16 blocks are left + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "padding", LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + memset(buffer, 0, 512); + while (LFS2_BLOCK_COUNT - lfs2_fs_size(&lfs2) > 16) { + lfs2_file_write(&lfs2, &file, buffer, 512) => 512; + } + lfs2_file_close(&lfs2, &file) => 0; + // make a child dir to use in bounded space + lfs2_mkdir(&lfs2, "child") => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + for (int j = 0; j < ITERATIONS; j++) { + for (int i = 0; i < COUNT; i++) { + sprintf(path, "child/test%03d_loooooooooooooooooong_name", i); + lfs2_file_open(&lfs2, &file, path, LFS2_O_CREAT | LFS2_O_WRONLY) => 0; + lfs2_file_close(&lfs2, &file) => 0; + } + + lfs2_dir_open(&lfs2, &dir, "child") => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + for (int i = 0; i < COUNT; i++) { + sprintf(path, "test%03d_loooooooooooooooooong_name", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + strcmp(info.name, path) => 0; + info.size => 0; + + sprintf(path, "child/test%03d_loooooooooooooooooong_name", i); + lfs2_file_open(&lfs2, &file, path, LFS2_O_WRONLY) => 0; + lfs2_file_write(&lfs2, &file, "hi", 2) => 2; + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + + lfs2_dir_rewind(&lfs2, &dir) => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + for (int i = 0; i < COUNT; i++) { + sprintf(path, "test%03d_loooooooooooooooooong_name", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + strcmp(info.name, path) => 0; + info.size => 2; + + sprintf(path, "child/test%03d_loooooooooooooooooong_name", i); + lfs2_file_open(&lfs2, &file, path, LFS2_O_WRONLY) => 0; + lfs2_file_write(&lfs2, &file, "hi", 2) => 2; + lfs2_file_close(&lfs2, &file) => 0; + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + + lfs2_dir_rewind(&lfs2, &dir) => 0; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + lfs2_dir_read(&lfs2, &dir, &info) => 1; + for (int i = 0; i < COUNT; i++) { + sprintf(path, "test%03d_loooooooooooooooooong_name", i); + lfs2_dir_read(&lfs2, &dir, &info) => 1; + strcmp(info.name, path) => 0; + info.size => 2; + } + lfs2_dir_read(&lfs2, &dir, &info) => 0; + lfs2_dir_close(&lfs2, &dir) => 0; + + for (int i = 0; i < COUNT; i++) { + sprintf(path, "child/test%03d_loooooooooooooooooong_name", i); + lfs2_remove(&lfs2, path) => 0; + } + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # reentrant testing for relocations, this is the same as the + # orphan testing, except here we also set block_cycles so that + # almost every tree operation needs a relocation +reentrant = true +# TODO fix this case, caused by non-DAG trees +if = '!(DEPTH == 3 && LFS2_CACHE_SIZE != 64)' +define = [ + {FILES=6, DEPTH=1, CYCLES=20, LFS2_BLOCK_CYCLES=1}, + {FILES=26, DEPTH=1, CYCLES=20, LFS2_BLOCK_CYCLES=1}, + {FILES=3, DEPTH=3, CYCLES=20, LFS2_BLOCK_CYCLES=1}, +] +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + srand(1); + const char alpha[] = "abcdefghijklmnopqrstuvwxyz"; + for (int i = 0; i < CYCLES; i++) { + // create random path + char full_path[256]; + for (int d = 0; d < DEPTH; d++) { + sprintf(&full_path[2*d], "/%c", alpha[rand() % FILES]); + } + + // if it does not exist, we create it, else we destroy + int res = lfs2_stat(&lfs2, full_path, &info); + if (res == LFS2_ERR_NOENT) { + // create each directory in turn, ignore if dir already exists + for (int d = 0; d < DEPTH; d++) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + err = lfs2_mkdir(&lfs2, path); + assert(!err || err == LFS2_ERR_EXIST); + } + + for (int d = 0; d < DEPTH; d++) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + lfs2_stat(&lfs2, path, &info) => 0; + assert(strcmp(info.name, &path[2*d+1]) == 0); + assert(info.type == LFS2_TYPE_DIR); + } + } else { + // is valid dir? + assert(strcmp(info.name, &full_path[2*(DEPTH-1)+1]) == 0); + assert(info.type == LFS2_TYPE_DIR); + + // try to delete path in reverse order, ignore if dir is not empty + for (int d = DEPTH-1; d >= 0; d--) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + err = lfs2_remove(&lfs2, path); + assert(!err || err == LFS2_ERR_NOTEMPTY); + } + + lfs2_stat(&lfs2, full_path, &info) => LFS2_ERR_NOENT; + } + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # reentrant testing for relocations, but now with random renames! +reentrant = true +# TODO fix this case, caused by non-DAG trees +if = '!(DEPTH == 3 && LFS2_CACHE_SIZE != 64)' +define = [ + {FILES=6, DEPTH=1, CYCLES=20, LFS2_BLOCK_CYCLES=1}, + {FILES=26, DEPTH=1, CYCLES=20, LFS2_BLOCK_CYCLES=1}, + {FILES=3, DEPTH=3, CYCLES=20, LFS2_BLOCK_CYCLES=1}, +] +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + srand(1); + const char alpha[] = "abcdefghijklmnopqrstuvwxyz"; + for (int i = 0; i < CYCLES; i++) { + // create random path + char full_path[256]; + for (int d = 0; d < DEPTH; d++) { + sprintf(&full_path[2*d], "/%c", alpha[rand() % FILES]); + } + + // if it does not exist, we create it, else we destroy + int res = lfs2_stat(&lfs2, full_path, &info); + assert(!res || res == LFS2_ERR_NOENT); + if (res == LFS2_ERR_NOENT) { + // create each directory in turn, ignore if dir already exists + for (int d = 0; d < DEPTH; d++) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + err = lfs2_mkdir(&lfs2, path); + assert(!err || err == LFS2_ERR_EXIST); + } + + for (int d = 0; d < DEPTH; d++) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + lfs2_stat(&lfs2, path, &info) => 0; + assert(strcmp(info.name, &path[2*d+1]) == 0); + assert(info.type == LFS2_TYPE_DIR); + } + } else { + assert(strcmp(info.name, &full_path[2*(DEPTH-1)+1]) == 0); + assert(info.type == LFS2_TYPE_DIR); + + // create new random path + char new_path[256]; + for (int d = 0; d < DEPTH; d++) { + sprintf(&new_path[2*d], "/%c", alpha[rand() % FILES]); + } + + // if new path does not exist, rename, otherwise destroy + res = lfs2_stat(&lfs2, new_path, &info); + assert(!res || res == LFS2_ERR_NOENT); + if (res == LFS2_ERR_NOENT) { + // stop once some dir is renamed + for (int d = 0; d < DEPTH; d++) { + strcpy(&path[2*d], &full_path[2*d]); + path[2*d+2] = '\0'; + strcpy(&path[128+2*d], &new_path[2*d]); + path[128+2*d+2] = '\0'; + err = lfs2_rename(&lfs2, path, path+128); + assert(!err || err == LFS2_ERR_NOTEMPTY); + if (!err) { + strcpy(path, path+128); + } + } + + for (int d = 0; d < DEPTH; d++) { + strcpy(path, new_path); + path[2*d+2] = '\0'; + lfs2_stat(&lfs2, path, &info) => 0; + assert(strcmp(info.name, &path[2*d+1]) == 0); + assert(info.type == LFS2_TYPE_DIR); + } + + lfs2_stat(&lfs2, full_path, &info) => LFS2_ERR_NOENT; + } else { + // try to delete path in reverse order, + // ignore if dir is not empty + for (int d = DEPTH-1; d >= 0; d--) { + strcpy(path, full_path); + path[2*d+2] = '\0'; + err = lfs2_remove(&lfs2, path); + assert(!err || err == LFS2_ERR_NOTEMPTY); + } + + lfs2_stat(&lfs2, full_path, &info) => LFS2_ERR_NOENT; + } + } + } + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_seek.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_seek.toml new file mode 100644 index 00000000000..b2c5fc17805 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_seek.toml @@ -0,0 +1,380 @@ + +[[case]] # simple file seek +define = [ + {COUNT=132, SKIP=4}, + {COUNT=132, SKIP=128}, + {COUNT=200, SKIP=10}, + {COUNT=200, SKIP=100}, + {COUNT=4, SKIP=1}, + {COUNT=4, SKIP=2}, +] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "kitty", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + size = strlen("kittycatcat"); + memcpy(buffer, "kittycatcat", size); + for (int j = 0; j < COUNT; j++) { + lfs2_file_write(&lfs2, &file, buffer, size); + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "kitty", LFS2_O_RDONLY) => 0; + + lfs2_soff_t pos = -1; + size = strlen("kittycatcat"); + for (int i = 0; i < SKIP; i++) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + pos = lfs2_file_tell(&lfs2, &file); + } + assert(pos >= 0); + + lfs2_file_seek(&lfs2, &file, pos, LFS2_SEEK_SET) => pos; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_rewind(&lfs2, &file) => 0; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_CUR) => size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_seek(&lfs2, &file, size, LFS2_SEEK_CUR) => 3*size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_seek(&lfs2, &file, pos, LFS2_SEEK_SET) => pos; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_seek(&lfs2, &file, -size, LFS2_SEEK_CUR) => pos; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_seek(&lfs2, &file, -size, LFS2_SEEK_END) >= 0 => 1; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + size = lfs2_file_size(&lfs2, &file); + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_CUR) => size; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # simple file seek and write +define = [ + {COUNT=132, SKIP=4}, + {COUNT=132, SKIP=128}, + {COUNT=200, SKIP=10}, + {COUNT=200, SKIP=100}, + {COUNT=4, SKIP=1}, + {COUNT=4, SKIP=2}, +] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "kitty", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + size = strlen("kittycatcat"); + memcpy(buffer, "kittycatcat", size); + for (int j = 0; j < COUNT; j++) { + lfs2_file_write(&lfs2, &file, buffer, size); + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "kitty", LFS2_O_RDWR) => 0; + + lfs2_soff_t pos = -1; + size = strlen("kittycatcat"); + for (int i = 0; i < SKIP; i++) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + pos = lfs2_file_tell(&lfs2, &file); + } + assert(pos >= 0); + + memcpy(buffer, "doggodogdog", size); + lfs2_file_seek(&lfs2, &file, pos, LFS2_SEEK_SET) => pos; + lfs2_file_write(&lfs2, &file, buffer, size) => size; + + lfs2_file_seek(&lfs2, &file, pos, LFS2_SEEK_SET) => pos; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "doggodogdog", size) => 0; + + lfs2_file_rewind(&lfs2, &file) => 0; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_seek(&lfs2, &file, pos, LFS2_SEEK_SET) => pos; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "doggodogdog", size) => 0; + + lfs2_file_seek(&lfs2, &file, -size, LFS2_SEEK_END) >= 0 => 1; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + size = lfs2_file_size(&lfs2, &file); + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_CUR) => size; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # boundary seek and writes +define.COUNT = 132 +define.OFFSETS = '"{512, 1020, 513, 1021, 511, 1019, 1441}"' +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "kitty", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + size = strlen("kittycatcat"); + memcpy(buffer, "kittycatcat", size); + for (int j = 0; j < COUNT; j++) { + lfs2_file_write(&lfs2, &file, buffer, size); + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "kitty", LFS2_O_RDWR) => 0; + + size = strlen("hedgehoghog"); + const lfs2_soff_t offsets[] = OFFSETS; + + for (unsigned i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) { + lfs2_soff_t off = offsets[i]; + memcpy(buffer, "hedgehoghog", size); + lfs2_file_seek(&lfs2, &file, off, LFS2_SEEK_SET) => off; + lfs2_file_write(&lfs2, &file, buffer, size) => size; + lfs2_file_seek(&lfs2, &file, off, LFS2_SEEK_SET) => off; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "hedgehoghog", size) => 0; + + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_SET) => 0; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_seek(&lfs2, &file, off, LFS2_SEEK_SET) => off; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "hedgehoghog", size) => 0; + + lfs2_file_sync(&lfs2, &file) => 0; + + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_SET) => 0; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "kittycatcat", size) => 0; + + lfs2_file_seek(&lfs2, &file, off, LFS2_SEEK_SET) => off; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "hedgehoghog", size) => 0; + } + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # out of bounds seek +define = [ + {COUNT=132, SKIP=4}, + {COUNT=132, SKIP=128}, + {COUNT=200, SKIP=10}, + {COUNT=200, SKIP=100}, + {COUNT=4, SKIP=2}, + {COUNT=4, SKIP=3}, +] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "kitty", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_APPEND) => 0; + size = strlen("kittycatcat"); + memcpy(buffer, "kittycatcat", size); + for (int j = 0; j < COUNT; j++) { + lfs2_file_write(&lfs2, &file, buffer, size); + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "kitty", LFS2_O_RDWR) => 0; + + size = strlen("kittycatcat"); + lfs2_file_size(&lfs2, &file) => COUNT*size; + lfs2_file_seek(&lfs2, &file, (COUNT+SKIP)*size, + LFS2_SEEK_SET) => (COUNT+SKIP)*size; + lfs2_file_read(&lfs2, &file, buffer, size) => 0; + + memcpy(buffer, "porcupineee", size); + lfs2_file_write(&lfs2, &file, buffer, size) => size; + + lfs2_file_seek(&lfs2, &file, (COUNT+SKIP)*size, + LFS2_SEEK_SET) => (COUNT+SKIP)*size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "porcupineee", size) => 0; + + lfs2_file_seek(&lfs2, &file, COUNT*size, + LFS2_SEEK_SET) => COUNT*size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "\0\0\0\0\0\0\0\0\0\0\0", size) => 0; + + lfs2_file_seek(&lfs2, &file, -((COUNT+SKIP)*size), + LFS2_SEEK_CUR) => LFS2_ERR_INVAL; + lfs2_file_tell(&lfs2, &file) => (COUNT+1)*size; + + lfs2_file_seek(&lfs2, &file, -((COUNT+2*SKIP)*size), + LFS2_SEEK_END) => LFS2_ERR_INVAL; + lfs2_file_tell(&lfs2, &file) => (COUNT+1)*size; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # inline write and seek +define.SIZE = [2, 4, 128, 132] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "tinykitty", + LFS2_O_RDWR | LFS2_O_CREAT) => 0; + int j = 0; + int k = 0; + + memcpy(buffer, "abcdefghijklmnopqrstuvwxyz", 26); + for (unsigned i = 0; i < SIZE; i++) { + lfs2_file_write(&lfs2, &file, &buffer[j++ % 26], 1) => 1; + lfs2_file_tell(&lfs2, &file) => i+1; + lfs2_file_size(&lfs2, &file) => i+1; + } + + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_SET) => 0; + lfs2_file_tell(&lfs2, &file) => 0; + lfs2_file_size(&lfs2, &file) => SIZE; + for (unsigned i = 0; i < SIZE; i++) { + uint8_t c; + lfs2_file_read(&lfs2, &file, &c, 1) => 1; + c => buffer[k++ % 26]; + } + + lfs2_file_sync(&lfs2, &file) => 0; + lfs2_file_tell(&lfs2, &file) => SIZE; + lfs2_file_size(&lfs2, &file) => SIZE; + + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_SET) => 0; + for (unsigned i = 0; i < SIZE; i++) { + lfs2_file_write(&lfs2, &file, &buffer[j++ % 26], 1) => 1; + lfs2_file_tell(&lfs2, &file) => i+1; + lfs2_file_size(&lfs2, &file) => SIZE; + lfs2_file_sync(&lfs2, &file) => 0; + lfs2_file_tell(&lfs2, &file) => i+1; + lfs2_file_size(&lfs2, &file) => SIZE; + if (i < SIZE-2) { + uint8_t c[3]; + lfs2_file_seek(&lfs2, &file, -1, LFS2_SEEK_CUR) => i; + lfs2_file_read(&lfs2, &file, &c, 3) => 3; + lfs2_file_tell(&lfs2, &file) => i+3; + lfs2_file_size(&lfs2, &file) => SIZE; + lfs2_file_seek(&lfs2, &file, i+1, LFS2_SEEK_SET) => i+1; + lfs2_file_tell(&lfs2, &file) => i+1; + lfs2_file_size(&lfs2, &file) => SIZE; + } + } + + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_SET) => 0; + lfs2_file_tell(&lfs2, &file) => 0; + lfs2_file_size(&lfs2, &file) => SIZE; + for (unsigned i = 0; i < SIZE; i++) { + uint8_t c; + lfs2_file_read(&lfs2, &file, &c, 1) => 1; + c => buffer[k++ % 26]; + } + + lfs2_file_sync(&lfs2, &file) => 0; + lfs2_file_tell(&lfs2, &file) => SIZE; + lfs2_file_size(&lfs2, &file) => SIZE; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # file seek and write with power-loss +# must be power-of-2 for quadratic probing to be exhaustive +define.COUNT = [4, 64, 128] +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + err = lfs2_file_open(&lfs2, &file, "kitty", LFS2_O_RDONLY); + assert(!err || err == LFS2_ERR_NOENT); + if (!err) { + if (lfs2_file_size(&lfs2, &file) != 0) { + lfs2_file_size(&lfs2, &file) => 11*COUNT; + for (int j = 0; j < COUNT; j++) { + memset(buffer, 0, 11+1); + lfs2_file_read(&lfs2, &file, buffer, 11) => 11; + assert(memcmp(buffer, "kittycatcat", 11) == 0 || + memcmp(buffer, "doggodogdog", 11) == 0); + } + } + lfs2_file_close(&lfs2, &file) => 0; + } + + lfs2_file_open(&lfs2, &file, "kitty", LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + if (lfs2_file_size(&lfs2, &file) == 0) { + for (int j = 0; j < COUNT; j++) { + strcpy((char*)buffer, "kittycatcat"); + size = strlen((char*)buffer); + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + } + lfs2_file_close(&lfs2, &file) => 0; + + strcpy((char*)buffer, "doggodogdog"); + size = strlen((char*)buffer); + + lfs2_file_open(&lfs2, &file, "kitty", LFS2_O_RDWR) => 0; + lfs2_file_size(&lfs2, &file) => COUNT*size; + // seek and write using quadratic probing to touch all + // 11-byte words in the file + lfs2_off_t off = 0; + for (int j = 0; j < COUNT; j++) { + off = (5*off + 1) % COUNT; + lfs2_file_seek(&lfs2, &file, off*size, LFS2_SEEK_SET) => off*size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(memcmp(buffer, "kittycatcat", size) == 0 || + memcmp(buffer, "doggodogdog", size) == 0); + if (memcmp(buffer, "doggodogdog", size) != 0) { + lfs2_file_seek(&lfs2, &file, off*size, LFS2_SEEK_SET) => off*size; + strcpy((char*)buffer, "doggodogdog"); + lfs2_file_write(&lfs2, &file, buffer, size) => size; + lfs2_file_seek(&lfs2, &file, off*size, LFS2_SEEK_SET) => off*size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(memcmp(buffer, "doggodogdog", size) == 0); + lfs2_file_sync(&lfs2, &file) => 0; + lfs2_file_seek(&lfs2, &file, off*size, LFS2_SEEK_SET) => off*size; + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(memcmp(buffer, "doggodogdog", size) == 0); + } + } + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "kitty", LFS2_O_RDWR) => 0; + lfs2_file_size(&lfs2, &file) => COUNT*size; + for (int j = 0; j < COUNT; j++) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + assert(memcmp(buffer, "doggodogdog", size) == 0); + } + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_superblocks.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_superblocks.toml new file mode 100644 index 00000000000..797d7adf2f6 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_superblocks.toml @@ -0,0 +1,127 @@ +[[case]] # simple formatting test +code = ''' + lfs2_format(&lfs2, &cfg) => 0; +''' + +[[case]] # mount/unmount +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # reentrant format +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # invalid mount +code = ''' + lfs2_mount(&lfs2, &cfg) => LFS2_ERR_CORRUPT; +''' + +[[case]] # expanding superblock +define.LFS2_BLOCK_CYCLES = [32, 33, 1] +define.N = [10, 100, 1000] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + lfs2_file_open(&lfs2, &file, "dummy", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_stat(&lfs2, "dummy", &info) => 0; + assert(strcmp(info.name, "dummy") == 0); + assert(info.type == LFS2_TYPE_REG); + lfs2_remove(&lfs2, "dummy") => 0; + } + lfs2_unmount(&lfs2) => 0; + + // one last check after power-cycle + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "dummy", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_stat(&lfs2, "dummy", &info) => 0; + assert(strcmp(info.name, "dummy") == 0); + assert(info.type == LFS2_TYPE_REG); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # expanding superblock with power cycle +define.LFS2_BLOCK_CYCLES = [32, 33, 1] +define.N = [10, 100, 1000] +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + for (int i = 0; i < N; i++) { + lfs2_mount(&lfs2, &cfg) => 0; + // remove lingering dummy? + err = lfs2_stat(&lfs2, "dummy", &info); + assert(err == 0 || (err == LFS2_ERR_NOENT && i == 0)); + if (!err) { + assert(strcmp(info.name, "dummy") == 0); + assert(info.type == LFS2_TYPE_REG); + lfs2_remove(&lfs2, "dummy") => 0; + } + + lfs2_file_open(&lfs2, &file, "dummy", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_stat(&lfs2, "dummy", &info) => 0; + assert(strcmp(info.name, "dummy") == 0); + assert(info.type == LFS2_TYPE_REG); + lfs2_unmount(&lfs2) => 0; + } + + // one last check after power-cycle + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "dummy", &info) => 0; + assert(strcmp(info.name, "dummy") == 0); + assert(info.type == LFS2_TYPE_REG); + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # reentrant expanding superblock +define.LFS2_BLOCK_CYCLES = [2, 1] +define.N = 24 +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + + for (int i = 0; i < N; i++) { + // remove lingering dummy? + err = lfs2_stat(&lfs2, "dummy", &info); + assert(err == 0 || (err == LFS2_ERR_NOENT && i == 0)); + if (!err) { + assert(strcmp(info.name, "dummy") == 0); + assert(info.type == LFS2_TYPE_REG); + lfs2_remove(&lfs2, "dummy") => 0; + } + + lfs2_file_open(&lfs2, &file, "dummy", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_EXCL) => 0; + lfs2_file_close(&lfs2, &file) => 0; + lfs2_stat(&lfs2, "dummy", &info) => 0; + assert(strcmp(info.name, "dummy") == 0); + assert(info.type == LFS2_TYPE_REG); + } + + lfs2_unmount(&lfs2) => 0; + + // one last check after power-cycle + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_stat(&lfs2, "dummy", &info) => 0; + assert(strcmp(info.name, "dummy") == 0); + assert(info.type == LFS2_TYPE_REG); + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/littlefs/tests/test_truncate.toml b/features/storage/filesystem/littlefsv2/littlefs/tests/test_truncate.toml new file mode 100644 index 00000000000..db664cb75e2 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/littlefs/tests/test_truncate.toml @@ -0,0 +1,394 @@ +[[case]] # simple truncate +define.MEDIUMSIZE = [32, 2048] +define.LARGESIZE = 8192 +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldynoop", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + + strcpy((char*)buffer, "hair"); + size = strlen((char*)buffer); + for (lfs2_off_t j = 0; j < LARGESIZE; j += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_size(&lfs2, &file) => LARGESIZE; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldynoop", LFS2_O_RDWR) => 0; + lfs2_file_size(&lfs2, &file) => LARGESIZE; + + lfs2_file_truncate(&lfs2, &file, MEDIUMSIZE) => 0; + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldynoop", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + + size = strlen("hair"); + for (lfs2_off_t j = 0; j < MEDIUMSIZE; j += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "hair", size) => 0; + } + lfs2_file_read(&lfs2, &file, buffer, size) => 0; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # truncate and read +define.MEDIUMSIZE = [32, 2048] +define.LARGESIZE = 8192 +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldyread", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + + strcpy((char*)buffer, "hair"); + size = strlen((char*)buffer); + for (lfs2_off_t j = 0; j < LARGESIZE; j += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_size(&lfs2, &file) => LARGESIZE; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldyread", LFS2_O_RDWR) => 0; + lfs2_file_size(&lfs2, &file) => LARGESIZE; + + lfs2_file_truncate(&lfs2, &file, MEDIUMSIZE) => 0; + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + + size = strlen("hair"); + for (lfs2_off_t j = 0; j < MEDIUMSIZE; j += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "hair", size) => 0; + } + lfs2_file_read(&lfs2, &file, buffer, size) => 0; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldyread", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + + size = strlen("hair"); + for (lfs2_off_t j = 0; j < MEDIUMSIZE; j += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "hair", size) => 0; + } + lfs2_file_read(&lfs2, &file, buffer, size) => 0; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # write, truncate, and read +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "sequence", + LFS2_O_RDWR | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + + size = lfs2_min(lfs2.cfg->cache_size, sizeof(buffer)/2); + lfs2_size_t qsize = size / 4; + uint8_t *wb = buffer; + uint8_t *rb = buffer + size; + for (lfs2_off_t j = 0; j < size; ++j) { + wb[j] = j; + } + + /* Spread sequence over size */ + lfs2_file_write(&lfs2, &file, wb, size) => size; + lfs2_file_size(&lfs2, &file) => size; + lfs2_file_tell(&lfs2, &file) => size; + + lfs2_file_seek(&lfs2, &file, 0, LFS2_SEEK_SET) => 0; + lfs2_file_tell(&lfs2, &file) => 0; + + /* Chop off the last quarter */ + lfs2_size_t trunc = size - qsize; + lfs2_file_truncate(&lfs2, &file, trunc) => 0; + lfs2_file_tell(&lfs2, &file) => 0; + lfs2_file_size(&lfs2, &file) => trunc; + + /* Read should produce first 3/4 */ + lfs2_file_read(&lfs2, &file, rb, size) => trunc; + memcmp(rb, wb, trunc) => 0; + + /* Move to 1/4 */ + lfs2_file_size(&lfs2, &file) => trunc; + lfs2_file_seek(&lfs2, &file, qsize, LFS2_SEEK_SET) => qsize; + lfs2_file_tell(&lfs2, &file) => qsize; + + /* Chop to 1/2 */ + trunc -= qsize; + lfs2_file_truncate(&lfs2, &file, trunc) => 0; + lfs2_file_tell(&lfs2, &file) => qsize; + lfs2_file_size(&lfs2, &file) => trunc; + + /* Read should produce second quarter */ + lfs2_file_read(&lfs2, &file, rb, size) => trunc - qsize; + memcmp(rb, wb + qsize, trunc - qsize) => 0; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # truncate and write +define.MEDIUMSIZE = [32, 2048] +define.LARGESIZE = 8192 +code = ''' + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldywrite", + LFS2_O_WRONLY | LFS2_O_CREAT) => 0; + + strcpy((char*)buffer, "hair"); + size = strlen((char*)buffer); + for (lfs2_off_t j = 0; j < LARGESIZE; j += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_size(&lfs2, &file) => LARGESIZE; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldywrite", LFS2_O_RDWR) => 0; + lfs2_file_size(&lfs2, &file) => LARGESIZE; + + lfs2_file_truncate(&lfs2, &file, MEDIUMSIZE) => 0; + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + + strcpy((char*)buffer, "bald"); + size = strlen((char*)buffer); + for (lfs2_off_t j = 0; j < MEDIUMSIZE; j += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + lfs2_file_open(&lfs2, &file, "baldywrite", LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + + size = strlen("bald"); + for (lfs2_off_t j = 0; j < MEDIUMSIZE; j += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "bald", size) => 0; + } + lfs2_file_read(&lfs2, &file, buffer, size) => 0; + + lfs2_file_close(&lfs2, &file) => 0; + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # truncate write under powerloss +define.SMALLSIZE = [4, 512] +define.MEDIUMSIZE = [32, 1024] +define.LARGESIZE = 2048 +reentrant = true +code = ''' + err = lfs2_mount(&lfs2, &cfg); + if (err) { + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + } + err = lfs2_file_open(&lfs2, &file, "baldy", LFS2_O_RDONLY); + assert(!err || err == LFS2_ERR_NOENT); + if (!err) { + size = lfs2_file_size(&lfs2, &file); + assert(size == 0 || + size == LARGESIZE || + size == MEDIUMSIZE || + size == SMALLSIZE); + for (lfs2_off_t j = 0; j < size; j += 4) { + lfs2_file_read(&lfs2, &file, buffer, 4) => 4; + assert(memcmp(buffer, "hair", 4) == 0 || + memcmp(buffer, "bald", 4) == 0 || + memcmp(buffer, "comb", 4) == 0); + } + lfs2_file_close(&lfs2, &file) => 0; + } + + lfs2_file_open(&lfs2, &file, "baldy", + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + lfs2_file_size(&lfs2, &file) => 0; + strcpy((char*)buffer, "hair"); + size = strlen((char*)buffer); + for (lfs2_off_t j = 0; j < LARGESIZE; j += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_size(&lfs2, &file) => LARGESIZE; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "baldy", LFS2_O_RDWR) => 0; + lfs2_file_size(&lfs2, &file) => LARGESIZE; + lfs2_file_truncate(&lfs2, &file, MEDIUMSIZE) => 0; + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + strcpy((char*)buffer, "bald"); + size = strlen((char*)buffer); + for (lfs2_off_t j = 0; j < MEDIUMSIZE; j += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_file_open(&lfs2, &file, "baldy", LFS2_O_RDWR) => 0; + lfs2_file_size(&lfs2, &file) => MEDIUMSIZE; + lfs2_file_truncate(&lfs2, &file, SMALLSIZE) => 0; + lfs2_file_size(&lfs2, &file) => SMALLSIZE; + strcpy((char*)buffer, "comb"); + size = strlen((char*)buffer); + for (lfs2_off_t j = 0; j < SMALLSIZE; j += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_size(&lfs2, &file) => SMALLSIZE; + lfs2_file_close(&lfs2, &file) => 0; + + lfs2_unmount(&lfs2) => 0; +''' + +[[case]] # more aggressive general truncation tests +define.CONFIG = 'range(6)' +define.SMALLSIZE = 32 +define.MEDIUMSIZE = 2048 +define.LARGESIZE = 8192 +code = ''' + #define COUNT 5 + const struct { + lfs2_off_t startsizes[COUNT]; + lfs2_off_t startseeks[COUNT]; + lfs2_off_t hotsizes[COUNT]; + lfs2_off_t coldsizes[COUNT]; + } configs[] = { + // cold shrinking + {{2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + { 0, SMALLSIZE, MEDIUMSIZE, LARGESIZE, 2*LARGESIZE}}, + // cold expanding + {{2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + { 0, SMALLSIZE, MEDIUMSIZE, LARGESIZE, 2*LARGESIZE}}, + // warm shrinking truncate + {{2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + { 0, SMALLSIZE, MEDIUMSIZE, LARGESIZE, 2*LARGESIZE}, + { 0, 0, 0, 0, 0}}, + // warm expanding truncate + {{ 0, SMALLSIZE, MEDIUMSIZE, LARGESIZE, 2*LARGESIZE}, + { 0, SMALLSIZE, MEDIUMSIZE, LARGESIZE, 2*LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}}, + // mid-file shrinking truncate + {{2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + { LARGESIZE, LARGESIZE, LARGESIZE, LARGESIZE, LARGESIZE}, + { 0, SMALLSIZE, MEDIUMSIZE, LARGESIZE, 2*LARGESIZE}, + { 0, 0, 0, 0, 0}}, + // mid-file expanding truncate + {{ 0, SMALLSIZE, MEDIUMSIZE, LARGESIZE, 2*LARGESIZE}, + { 0, 0, SMALLSIZE, MEDIUMSIZE, LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}, + {2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE, 2*LARGESIZE}}, + }; + + const lfs2_off_t *startsizes = configs[CONFIG].startsizes; + const lfs2_off_t *startseeks = configs[CONFIG].startseeks; + const lfs2_off_t *hotsizes = configs[CONFIG].hotsizes; + const lfs2_off_t *coldsizes = configs[CONFIG].coldsizes; + + lfs2_format(&lfs2, &cfg) => 0; + lfs2_mount(&lfs2, &cfg) => 0; + + for (unsigned i = 0; i < COUNT; i++) { + sprintf(path, "hairyhead%d", i); + lfs2_file_open(&lfs2, &file, path, + LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC) => 0; + + strcpy((char*)buffer, "hair"); + size = strlen((char*)buffer); + for (lfs2_off_t j = 0; j < startsizes[i]; j += size) { + lfs2_file_write(&lfs2, &file, buffer, size) => size; + } + lfs2_file_size(&lfs2, &file) => startsizes[i]; + + if (startseeks[i] != startsizes[i]) { + lfs2_file_seek(&lfs2, &file, + startseeks[i], LFS2_SEEK_SET) => startseeks[i]; + } + + lfs2_file_truncate(&lfs2, &file, hotsizes[i]) => 0; + lfs2_file_size(&lfs2, &file) => hotsizes[i]; + + lfs2_file_close(&lfs2, &file) => 0; + } + + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + + for (unsigned i = 0; i < COUNT; i++) { + sprintf(path, "hairyhead%d", i); + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDWR) => 0; + lfs2_file_size(&lfs2, &file) => hotsizes[i]; + + size = strlen("hair"); + lfs2_off_t j = 0; + for (; j < startsizes[i] && j < hotsizes[i]; j += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "hair", size) => 0; + } + + for (; j < hotsizes[i]; j += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "\0\0\0\0", size) => 0; + } + + lfs2_file_truncate(&lfs2, &file, coldsizes[i]) => 0; + lfs2_file_size(&lfs2, &file) => coldsizes[i]; + + lfs2_file_close(&lfs2, &file) => 0; + } + + lfs2_unmount(&lfs2) => 0; + + lfs2_mount(&lfs2, &cfg) => 0; + + for (unsigned i = 0; i < COUNT; i++) { + sprintf(path, "hairyhead%d", i); + lfs2_file_open(&lfs2, &file, path, LFS2_O_RDONLY) => 0; + lfs2_file_size(&lfs2, &file) => coldsizes[i]; + + size = strlen("hair"); + lfs2_off_t j = 0; + for (; j < startsizes[i] && j < hotsizes[i] && j < coldsizes[i]; + j += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "hair", size) => 0; + } + + for (; j < coldsizes[i]; j += size) { + lfs2_file_read(&lfs2, &file, buffer, size) => size; + memcmp(buffer, "\0\0\0\0", size) => 0; + } + + lfs2_file_close(&lfs2, &file) => 0; + } + + lfs2_unmount(&lfs2) => 0; +''' diff --git a/features/storage/filesystem/littlefsv2/mbed_lib.json b/features/storage/filesystem/littlefsv2/mbed_lib.json new file mode 100644 index 00000000000..16b2c12d5a0 --- /dev/null +++ b/features/storage/filesystem/littlefsv2/mbed_lib.json @@ -0,0 +1,55 @@ +{ + "name": "littlefs2", + "config": { + "block_size": { + "macro_name": "MBED_LFS2_BLOCK_SIZE", + "value": 512, + "help": "Size of a logical block. This does not impact ram consumption and may be larger than the physical erase block. If the physical erase block is larger, littlefs will use that instead. Larger values will be faster but waste more storage when files are not aligned to a block size." + }, + "block_cycles": { + "macro_name": "MBED_LFS2_BLOCK_CYCLES", + "value": 1024, + "help": "Number of erase cycles before a block is forcefully evicted. Larger values are more efficient but cause less even wear distribution. 0 disables dynamic wear-leveling." + }, + "cache_size": { + "macro_name": "MBED_LFS2_CACHE_SIZE", + "value": "64", + "help": "Size of read/program caches. Each file uses 1 cache, and littlefs allocates 2 caches for internal operations. Larger values should be faster but uses more RAM." + }, + "lookahead_size": { + "macro_name": "MBED_LFS2_LOOKAHEAD_SIZE", + "value": 64, + "help": "Size of the lookahead buffer. A larger lookahead reduces the allocation scans and results in a faster filesystem but uses more RAM." + }, + "intrinsics": { + "macro_name": "MBED_LFS2_INTRINSICS", + "value": true, + "help": "Enable intrinsics for bit operations such as ctz, popc, and le32 conversion. Can be disabled to help debug toolchain issues" + }, + "enable_info": { + "macro_name": "MBED_LFS2_ENABLE_INFO", + "value": false, + "help": "Enables info logging, true = enabled, false = disabled, null = disabled only in release builds" + }, + "enable_debug": { + "macro_name": "MBED_LFS2_ENABLE_DEBUG", + "value": null, + "help": "Enables debug logging, true = enabled, false = disabled, null = disabled only in release builds" + }, + "enable_warn": { + "macro_name": "MBED_LFS2_ENABLE_WARN", + "value": null, + "help": "Enables warn logging, true = enabled, false = disabled, null = disabled only in release builds" + }, + "enable_error": { + "macro_name": "MBED_LFS2_ENABLE_ERROR", + "value": null, + "help": "Enables error logging, true = enabled, false = disabled, null = disabled only in release builds" + }, + "enable_assert": { + "macro_name": "MBED_LFS2_ENABLE_ASSERT", + "value": null, + "help": "Enables asserts, true = enabled, false = disabled, null = disabled only in release builds" + } + } +}