diff --git a/.gitmodules b/.gitmodules index a3ba6c1ffa..ca26e7d93b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,9 @@ [submodule "libraries/SoftwareSerial"] path = libraries/SoftwareSerial url = https://github.com/plerup/espsoftwareserial.git +[submodule "libraries/LittleFS/lib/littlefs"] + path = libraries/LittleFS/lib/littlefs + url = https://github.com/ARMmbed/littlefs.git [submodule "libraries/ESP8266SdFat"] path = libraries/ESP8266SdFat url = https://github.com/earlephilhower/ESP8266SdFat.git diff --git a/README.md b/README.md index a347be7cb5..7079edd3ac 100644 --- a/README.md +++ b/README.md @@ -143,3 +143,5 @@ ESP8266 core files are licensed under LGPL. [axTLS](http://axtls.sourceforge.net/) library written by Cameron Rich, built from https://github.com/igrr/axtls-8266, is used in this project. It is distributed under [BSD license](https://github.com/igrr/axtls-8266/blob/master/LICENSE). [BearSSL](https://bearssl.org) library written by Thomas Pornin, built from https://github.com/earlephilhower/bearssl-esp8266, is used in this project. It is distributed under the [MIT License](https://bearssl.org/#legal-details). + +[LittleFS](https://github.com/ARMmbed/littlefs) library written by ARM Limited and released under the [BSD 3-clause license](https://github.com/ARMmbed/littlefs/blob/master/LICENSE.md). diff --git a/cores/esp8266/Esp.cpp b/cores/esp8266/Esp.cpp index 23d61d33fa..37c301458b 100644 --- a/cores/esp8266/Esp.cpp +++ b/cores/esp8266/Esp.cpp @@ -519,14 +519,14 @@ uint32_t EspClass::getSketchSize() { return result; } -extern "C" uint32_t _SPIFFS_start; +extern "C" uint32_t _FS_start; uint32_t EspClass::getFreeSketchSpace() { uint32_t usedSize = getSketchSize(); // round one sector up uint32_t freeSpaceStart = (usedSize + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); - uint32_t freeSpaceEnd = (uint32_t)&_SPIFFS_start - 0x40200000; + uint32_t freeSpaceEnd = (uint32_t)&_FS_start - 0x40200000; #ifdef DEBUG_SERIAL DEBUG_SERIAL.printf("usedSize=%u freeSpaceStart=%u freeSpaceEnd=%u\r\n", usedSize, freeSpaceStart, freeSpaceEnd); diff --git a/cores/esp8266/FS.h b/cores/esp8266/FS.h index 7bf71692cc..f30483d901 100644 --- a/cores/esp8266/FS.h +++ b/cores/esp8266/FS.h @@ -62,7 +62,7 @@ class File : public Stream int read() override; int peek() override; void flush() override; - size_t readBytes(char *buffer, size_t length) override { + size_t readBytes(char *buffer, size_t length) override { return read((uint8_t*)buffer, length); } size_t read(uint8_t* buf, size_t size); diff --git a/cores/esp8266/Updater.cpp b/cores/esp8266/Updater.cpp index 9f68541f44..42c308f181 100644 --- a/cores/esp8266/Updater.cpp +++ b/cores/esp8266/Updater.cpp @@ -24,7 +24,7 @@ extern "C" { #include "user_interface.h" } -extern "C" uint32_t _SPIFFS_start; +extern "C" uint32_t _FS_start; UpdaterClass::UpdaterClass() : _async(false) @@ -87,8 +87,8 @@ bool UpdaterClass::begin(size_t size, int command, int ledPin, uint8_t ledOn) { } #ifdef DEBUG_UPDATER - if (command == U_SPIFFS) { - DEBUG_UPDATER.println(F("[begin] Update SPIFFS.")); + if (command == U_FS) { + DEBUG_UPDATER.println(F("[begin] Update Filesystem.")); } #endif @@ -112,7 +112,7 @@ bool UpdaterClass::begin(size_t size, int command, int ledPin, uint8_t ledOn) { //size of current sketch rounded to a sector size_t currentSketchSize = (ESP.getSketchSize() + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); //address of the end of the space available for sketch and update - uintptr_t updateEndAddress = (uintptr_t)&_SPIFFS_start - 0x40200000; + uintptr_t updateEndAddress = (uintptr_t)&_FS_start - 0x40200000; //size of the update rounded to a sector size_t roundedSize = (size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); //address where we will start writing the update @@ -130,8 +130,8 @@ bool UpdaterClass::begin(size_t size, int command, int ledPin, uint8_t ledOn) { return false; } } - else if (command == U_SPIFFS) { - updateStartAddress = (uintptr_t)&_SPIFFS_start - 0x40200000; + else if (command == U_FS) { + updateStartAddress = (uintptr_t)&_FS_start - 0x40200000; } else { // unknown command @@ -279,8 +279,8 @@ bool UpdaterClass::end(bool evenIfRemaining){ #ifdef DEBUG_UPDATER DEBUG_UPDATER.printf_P(PSTR("Staged: address:0x%08X, size:0x%08zX\n"), _startAddress, _size); } - else if (_command == U_SPIFFS) { - DEBUG_UPDATER.printf_P(PSTR("SPIFFS: address:0x%08X, size:0x%08zX\n"), _startAddress, _size); + else if (_command == U_FS) { + DEBUG_UPDATER.printf_P(PSTR("Filesystem: address:0x%08X, size:0x%08zX\n"), _startAddress, _size); #endif } @@ -392,7 +392,7 @@ bool UpdaterClass::_verifyHeader(uint8_t data) { return false; } return true; - } else if(_command == U_SPIFFS) { + } else if(_command == U_FS) { // no check of SPIFFS possible with first byte. return true; } @@ -426,7 +426,7 @@ bool UpdaterClass::_verifyEnd() { } return true; - } else if(_command == U_SPIFFS) { + } else if(_command == U_FS) { // SPIFFS is already over written checks make no sense any more. return true; } diff --git a/cores/esp8266/Updater.h b/cores/esp8266/Updater.h index 8de16c7ed6..e2198e65d6 100644 --- a/cores/esp8266/Updater.h +++ b/cores/esp8266/Updater.h @@ -21,7 +21,7 @@ #define UPDATE_ERROR_SIGN (12) #define U_FLASH 0 -#define U_SPIFFS 100 +#define U_FS 100 #define U_AUTH 200 #ifdef DEBUG_ESP_UPDATER diff --git a/cores/esp8266/spiffs_hal.cpp b/cores/esp8266/flash_hal.cpp similarity index 90% rename from cores/esp8266/spiffs_hal.cpp rename to cores/esp8266/flash_hal.cpp index 2d66bd54df..4602f8309a 100644 --- a/cores/esp8266/spiffs_hal.cpp +++ b/cores/esp8266/flash_hal.cpp @@ -21,8 +21,8 @@ #include #include #include -#include "spiffs/spiffs.h" #include "debug.h" +#include "flash_hal.h" extern "C" { #include "c_types.h" @@ -42,10 +42,10 @@ alignedBegin: ^ alignedEnd: ^ */ -int32_t spiffs_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { +int32_t flash_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { optimistic_yield(10000); - uint32_t result = SPIFFS_OK; + uint32_t result = FLASH_HAL_OK; uint32_t alignedBegin = (addr + 3) & (~3); uint32_t alignedEnd = (addr + size) & (~3); if (alignedEnd < alignedBegin) { @@ -58,7 +58,7 @@ int32_t spiffs_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { if (!ESP.flashRead(alignedBegin - 4, &tmp, 4)) { DEBUGV("_spif_read(%d) addr=%x size=%x ab=%x ae=%x\r\n", __LINE__, addr, size, alignedBegin, alignedEnd); - return SPIFFS_ERR_INTERNAL; + return FLASH_HAL_READ_ERROR; } memcpy(dst, ((uint8_t*) &tmp) + 4 - nb, nb); } @@ -68,7 +68,7 @@ int32_t spiffs_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { alignedEnd - alignedBegin)) { DEBUGV("_spif_read(%d) addr=%x size=%x ab=%x ae=%x\r\n", __LINE__, addr, size, alignedBegin, alignedEnd); - return SPIFFS_ERR_INTERNAL; + return FLASH_HAL_READ_ERROR; } } @@ -78,7 +78,7 @@ int32_t spiffs_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { if (!ESP.flashRead(alignedEnd, &tmp, 4)) { DEBUGV("_spif_read(%d) addr=%x size=%x ab=%x ae=%x\r\n", __LINE__, addr, size, alignedBegin, alignedEnd); - return SPIFFS_ERR_INTERNAL; + return FLASH_HAL_READ_ERROR; } memcpy(dst + size - nb, &tmp, nb); @@ -99,7 +99,7 @@ int32_t spiffs_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { static const int UNALIGNED_WRITE_BUFFER_SIZE = 512; -int32_t spiffs_hal_write(uint32_t addr, uint32_t size, uint8_t *src) { +int32_t flash_hal_write(uint32_t addr, uint32_t size, const uint8_t *src) { optimistic_yield(10000); uint32_t alignedBegin = (addr + 3) & (~3); @@ -116,7 +116,7 @@ int32_t spiffs_hal_write(uint32_t addr, uint32_t size, uint8_t *src) { if (!ESP.flashWrite(alignedBegin - 4, (uint32_t*) tmp, 4)) { DEBUGV("_spif_write(%d) addr=%x size=%x ab=%x ae=%x\r\n", __LINE__, addr, size, alignedBegin, alignedEnd); - return SPIFFS_ERR_INTERNAL; + return FLASH_HAL_WRITE_ERROR; } } @@ -128,7 +128,7 @@ int32_t spiffs_hal_write(uint32_t addr, uint32_t size, uint8_t *src) { alignedEnd - alignedBegin)) { DEBUGV("_spif_write(%d) addr=%x size=%x ab=%x ae=%x\r\n", __LINE__, addr, size, alignedBegin, alignedEnd); - return SPIFFS_ERR_INTERNAL; + return FLASH_HAL_WRITE_ERROR; } } else { @@ -140,7 +140,7 @@ int32_t spiffs_hal_write(uint32_t addr, uint32_t size, uint8_t *src) { if (!ESP.flashWrite(alignedBegin, (uint32_t*) buf, willCopy)) { DEBUGV("_spif_write(%d) addr=%x size=%x ab=%x ae=%x\r\n", __LINE__, addr, size, alignedBegin, alignedEnd); - return SPIFFS_ERR_INTERNAL; + return FLASH_HAL_WRITE_ERROR; } sizeLeft -= willCopy; @@ -158,14 +158,14 @@ int32_t spiffs_hal_write(uint32_t addr, uint32_t size, uint8_t *src) { if (!ESP.flashWrite(alignedEnd, &tmp, 4)) { DEBUGV("_spif_write(%d) addr=%x size=%x ab=%x ae=%x\r\n", __LINE__, addr, size, alignedBegin, alignedEnd); - return SPIFFS_ERR_INTERNAL; + return FLASH_HAL_WRITE_ERROR; } } - return SPIFFS_OK; + return FLASH_HAL_OK; } -int32_t spiffs_hal_erase(uint32_t addr, uint32_t size) { +int32_t flash_hal_erase(uint32_t addr, uint32_t size) { if ((size & (SPI_FLASH_SEC_SIZE - 1)) != 0 || (addr & (SPI_FLASH_SEC_SIZE - 1)) != 0) { DEBUGV("_spif_erase called with addr=%x, size=%d\r\n", addr, size); @@ -177,8 +177,8 @@ int32_t spiffs_hal_erase(uint32_t addr, uint32_t size) { optimistic_yield(10000); if (!ESP.flashEraseSector(sector + i)) { DEBUGV("_spif_erase addr=%x size=%d i=%d\r\n", addr, size, i); - return SPIFFS_ERR_INTERNAL; + return FLASH_HAL_ERASE_ERROR; } } - return SPIFFS_OK; + return FLASH_HAL_OK; } diff --git a/cores/esp8266/flash_hal.h b/cores/esp8266/flash_hal.h new file mode 100644 index 0000000000..13219bb18f --- /dev/null +++ b/cores/esp8266/flash_hal.h @@ -0,0 +1,49 @@ +#ifndef flash_hal_h +#define flash_hal_h + +/* + flash_hal.h - API for accessing raw flash for filesystems + Copyright (c) 2015 Ivan Grokhotkov. All rights reserved. + + This code was influenced by NodeMCU and Sming libraries, and first version of + Arduino wrapper written by Hristo Gochkov. + + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +#ifdef ARDUINO +extern "C" uint32_t _FS_start; +extern "C" uint32_t _FS_end; +extern "C" uint32_t _FS_page; +extern "C" uint32_t _FS_block; + +#define FS_PHYS_ADDR ((uint32_t) (&_FS_start) - 0x40200000) +#define FS_PHYS_SIZE ((uint32_t) (&_FS_end) - (uint32_t) (&_FS_start)) +#define FS_PHYS_PAGE ((uint32_t) &_FS_page) +#define FS_PHYS_BLOCK ((uint32_t) &_FS_block) +#endif + +// Return values of the following functions +#define FLASH_HAL_OK (0) +#define FLASH_HAL_READ_ERROR (-1) +#define FLASH_HAL_WRITE_ERROR (-2) +#define FLASH_HAL_ERASE_ERROR (-3) + +extern int32_t flash_hal_write(uint32_t addr, uint32_t size, const uint8_t *src); +extern int32_t flash_hal_erase(uint32_t addr, uint32_t size); +extern int32_t flash_hal_read(uint32_t addr, uint32_t size, uint8_t *dst); + +#endif // !defined(flash_hal_h) diff --git a/cores/esp8266/spiffs/spiffs.h b/cores/esp8266/spiffs/spiffs.h index 534c3df8bd..e659bf2f37 100644 --- a/cores/esp8266/spiffs/spiffs.h +++ b/cores/esp8266/spiffs/spiffs.h @@ -84,7 +84,7 @@ struct spiffs_t; /* spi read call function type */ typedef s32_t (*spiffs_read)(struct spiffs_t *fs, u32_t addr, u32_t size, u8_t *dst); /* spi write call function type */ -typedef s32_t (*spiffs_write)(struct spiffs_t *fs, u32_t addr, u32_t size, u8_t *src); +typedef s32_t (*spiffs_write)(struct spiffs_t *fs, u32_t addr, u32_t size, const u8_t *src); /* spi erase call function type */ typedef s32_t (*spiffs_erase)(struct spiffs_t *fs, u32_t addr, u32_t size); @@ -93,7 +93,7 @@ typedef s32_t (*spiffs_erase)(struct spiffs_t *fs, u32_t addr, u32_t size); /* spi read call function type */ typedef s32_t (*spiffs_read)(u32_t addr, u32_t size, u8_t *dst); /* spi write call function type */ -typedef s32_t (*spiffs_write)(u32_t addr, u32_t size, u8_t *src); +typedef s32_t (*spiffs_write)(u32_t addr, u32_t size, const u8_t *src); /* spi erase call function type */ typedef s32_t (*spiffs_erase)(u32_t addr, u32_t size); #endif // SPIFFS_HAL_CALLBACK_EXTRA diff --git a/cores/esp8266/spiffs_api.cpp b/cores/esp8266/spiffs_api.cpp index f3fcfa2354..833dd3edb0 100644 --- a/cores/esp8266/spiffs_api.cpp +++ b/cores/esp8266/spiffs_api.cpp @@ -25,6 +25,8 @@ using namespace fs; +namespace spiffs_impl { + FileImplPtr SPIFFSImpl::open(const char* path, OpenMode openMode, AccessMode accessMode) { if (!isSpiffsFilenameValid(path)) { @@ -108,6 +110,8 @@ bool isSpiffsFilenameValid(const char* name) return len > 0 && len < SPIFFS_OBJ_NAME_LEN; } +}; // namespace + // these symbols should be defined in the linker script for each flash layout #ifndef CORE_MOCK #ifdef ARDUINO @@ -116,11 +120,11 @@ bool isSpiffsFilenameValid(const char* name) #endif #if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SPIFFS) -FS SPIFFS = FS(FSImplPtr(new SPIFFSImpl( - SPIFFS_PHYS_ADDR, - SPIFFS_PHYS_SIZE, - SPIFFS_PHYS_PAGE, - SPIFFS_PHYS_BLOCK, +FS SPIFFS = FS(FSImplPtr(new spiffs_impl::SPIFFSImpl( + FS_PHYS_ADDR, + FS_PHYS_SIZE, + FS_PHYS_PAGE, + FS_PHYS_BLOCK, SPIFFS_MAX_OPEN_FILES))); #endif // ARDUINO #endif // !CORE_MOCK diff --git a/cores/esp8266/spiffs_api.h b/cores/esp8266/spiffs_api.h index 95600a22ad..7312fa0867 100644 --- a/cores/esp8266/spiffs_api.h +++ b/cores/esp8266/spiffs_api.h @@ -35,24 +35,11 @@ extern "C" { }; #include "debug.h" #include "flash_utils.h" +#include "flash_hal.h" using namespace fs; -#ifdef ARDUINO -extern "C" uint32_t _SPIFFS_start; -extern "C" uint32_t _SPIFFS_end; -extern "C" uint32_t _SPIFFS_page; -extern "C" uint32_t _SPIFFS_block; - -#define SPIFFS_PHYS_ADDR ((uint32_t) (&_SPIFFS_start) - 0x40200000) -#define SPIFFS_PHYS_SIZE ((uint32_t) (&_SPIFFS_end) - (uint32_t) (&_SPIFFS_start)) -#define SPIFFS_PHYS_PAGE ((uint32_t) &_SPIFFS_page) -#define SPIFFS_PHYS_BLOCK ((uint32_t) &_SPIFFS_block) -#endif - -extern int32_t spiffs_hal_write(uint32_t addr, uint32_t size, uint8_t *src); -extern int32_t spiffs_hal_erase(uint32_t addr, uint32_t size); -extern int32_t spiffs_hal_read(uint32_t addr, uint32_t size, uint8_t *dst); +namespace spiffs_impl { int getSpiffsMode(OpenMode openMode, AccessMode accessMode); bool isSpiffsFilenameValid(const char* name); @@ -95,6 +82,7 @@ class SPIFFSImpl : public FSImpl } return true; } + bool info(FSInfo& info) override { info.maxOpenFiles = _maxOpenFds; @@ -150,6 +138,11 @@ class SPIFFSImpl : public FSImpl bool begin() override { +#if defined(ARDUINO) && !defined(CORE_MOCK) + if (&_FS_end <= &_FS_start) + return false; +#endif + if (SPIFFS_mounted(&_fs) != 0) { return true; } @@ -160,16 +153,16 @@ class SPIFFSImpl : public FSImpl if (_tryMount()) { return true; } - if (_cfg._autoFormat) { + if (_cfg._autoFormat) { auto rc = SPIFFS_format(&_fs); if (rc != SPIFFS_OK) { DEBUGV("SPIFFS_format: rc=%d, err=%d\r\n", rc, _fs.err_code); return false; } return _tryMount(); - } else { - return false; } + + return false; } void end() override @@ -303,6 +296,17 @@ class SPIFFSImpl : public FSImpl spiffs _fs; + // Flash hal wrapper functions to get proper SPIFFS error codes + static int32_t spiffs_hal_write(uint32_t addr, uint32_t size, const uint8_t *src) { + return flash_hal_write(addr, size, src) == FLASH_HAL_OK ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; + } + static int32_t spiffs_hal_erase(uint32_t addr, uint32_t size) { + return flash_hal_erase(addr, size) == FLASH_HAL_OK ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; + } + static int32_t spiffs_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { + return flash_hal_read(addr, size, dst) == FLASH_HAL_OK ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; + } + uint32_t _start; uint32_t _size; uint32_t _pageSize; @@ -560,4 +564,6 @@ class SPIFFSDirImpl : public DirImpl bool _valid; }; -#endif//spiffs_api_h +}; // namespace + +#endif //spiffs_api_h diff --git a/doc/filesystem.rst b/doc/filesystem.rst index 9e5c0537d3..b7d8b421ef 100644 --- a/doc/filesystem.rst +++ b/doc/filesystem.rst @@ -63,10 +63,39 @@ following include to the sketch: #include "FS.h" -File system limitations ------------------------ +SPIFFS and LittleFS +------------------- -The filesystem implementation for ESP8266 had to accomodate the +There are two filesystems for utilizing the onboard flash on the ESP8266: +SPIFFS and LittleFS. + +SPIFFS is the original filesystem and is ideal for space and RAM +constrained applications that utilize many small files and care +about static and dynamic wear levelling and don't need true directory +support. Filesystem overhead on the flash is minimal as well. + +LittleFS is recently added and focuses on higher performance and +directory support, but has higher filesystem and per-file overhead +(4K minimum vs. SPIFFS' 256 byte minimum file allocation unit). + +They share a compatible API but have incompatible on-flash +implementations, so it is important to choose one or the per project +as attempting to mount a SPIFFS volume under LittleFS may result +in a format operation and definitely will not preserve any files, +and vice-versa. + +The actual ``File`` and ``Dir`` objects returned from either +filesystem behave in the same manner and documentation is applicable +to both. To convert most applications from SPIFFS to LittleFS +simply requires changing the ``SPIFFS.begin()`` to ``LittleFS.begin()`` +and ``SPIFFS.open()`` to ``LittleFS.open()`` with the rest of the +code remaining untouched. + + +SPIFFS file system limitations +------------------------------ + +The SPIFFS implementation for ESP8266 had to accomodate the constraints of the chip, among which its limited RAM. `SPIFFS `__ was selected because it is designed for small systems, but that comes at the cost of some @@ -100,6 +129,37 @@ For more details on the internals of SPIFFS implementation, see the `SPIFFS readme file `__. + +LittleFS file system limitations +-------------------------------- + +The LittleFS implementation for the ESP8266 supports filenames of up +to 31 characters + terminating zero (i.e. ``char filename[32]``), and +as many subdirectories as space permits. + +Filenames are assumed to be in the root directory if no initial "/" is +present. + +Opening files in subdirectories requires specifying the complete path to +the file (i.e. ``open("/sub/dir/file.txt");``). Subdirectories are +automatically created when you attempt to create a file in a subdirectory, +and when the last file in a subdirectory is removed the subdirectory +itself is automatically deleted. This is because there was no ``mkdir()`` +method in the existing SPIFFS filesystem. + +Unlike SPIFFS, the actual file descriptors are allocated as requested +by the application, so in low memory conditions you may not be able to +open new files. Conversely, this also means that only file descriptors +used will actually take space on the heap. + +Because there are directories, the ``openDir`` method behaves differently +than SPIFFS. Whereas SPIFFS will return files in "subdirectories" when +you traverse a ``Dir::next()`` (because they really aren't subdirs but +simply files with "/"s in their names), LittleFS will only return files +in the specific subdirectory. This mimics the POSIX behavior for +directory traversal most C programmers are used to. + + Uploading files to file system ------------------------------ @@ -129,8 +189,15 @@ use esptool.py. uploading the files into ESP8266 flash file system. When done, IDE status bar will display ``SPIFFS Image Uploaded`` message. -File system object (SPIFFS) ---------------------------- +*ESP8266LittleFS* is the equivalent tool for LittleFS. + +- Download the tool: https://github.com/earlephilhower/arduino-esp8266littlefs-plugin/releases +- Install as above +- To upload a LittleFS filesystem use Tools > ESP8266 LittleFS Data Upload + + +File system object (SPIFFS/LittleFS) +------------------------------------ setConfig ~~~~~~~~~ @@ -156,21 +223,28 @@ begin .. code:: cpp SPIFFS.begin() + or LittleFS.begin() -This method mounts SPIFFS file system. It must be called before any +This method mounts file system. It must be called before any other FS APIs are used. Returns *true* if file system was mounted successfully, false otherwise. With no options it will format SPIFFS if it is unable to mount it on the first try. +Note that both methods will automatically format the filesystem +if one is not detected. This means that if you attempt a +``SPIFFS.begin()`` on a LittleFS filesystem you will lose all data +on that filesystem, and vice-versa. + end ~~~ .. code:: cpp SPIFFS.end() + or LittleFS.end() -This method unmounts SPIFFS file system. Use this method before updating -SPIFFS using OTA. +This method unmounts the file system. Use this method before updating +the file system using OTA. format ~~~~~~ @@ -178,6 +252,7 @@ format .. code:: cpp SPIFFS.format() + or LittleFS.format() Formats the file system. May be called either before or after calling ``begin``. Returns *true* if formatting was successful. @@ -188,6 +263,7 @@ open .. code:: cpp SPIFFS.open(path, mode) + or LittleFS.open(path, mode) Opens a file. ``path`` should be an absolute path starting with a slash (e.g. ``/dir/filename.txt``). ``mode`` is a string specifying access @@ -234,17 +310,40 @@ exists .. code:: cpp SPIFFS.exists(path) + or LittleFS.exists(path) Returns *true* if a file with given path exists, *false* otherwise. +mkdir +~~~~~ + +.. code:: cpp + + LittleFS.mkdir(path) + +Returns *true* if the directory creation succeeded, *false* otherwise. + +rmdir +~~~~~ + +.. code:: cpp + + LittleFS.rmdir(path) + +Returns *true* if the directory was successfully removed, *false* otherwise. + + openDir ~~~~~~~ .. code:: cpp SPIFFS.openDir(path) + or LittleFS.openDir(path) Opens a directory given its absolute path. Returns a *Dir* object. +Please note the previous discussion on the difference in behavior between +LittleFS and SPIFFS for this call. remove ~~~~~~ @@ -252,6 +351,7 @@ remove .. code:: cpp SPIFFS.remove(path) + or LittleFS.remove(path) Deletes the file given its absolute path. Returns *true* if file was deleted successfully. @@ -262,6 +362,7 @@ rename .. code:: cpp SPIFFS.rename(pathFrom, pathTo) + or LittleFS.rename(pathFrom, pathTo) Renames file from ``pathFrom`` to ``pathTo``. Paths must be absolute. Returns *true* if file was renamed successfully. @@ -273,6 +374,7 @@ info FSInfo fs_info; SPIFFS.info(fs_info); + or LittleFS.info(fs_info); Fills `FSInfo structure <#filesystem-information-structure>`__ with information about the file system. Returns ``true`` is successful, @@ -294,8 +396,8 @@ Filesystem information structure This is the structure which may be filled using FS::info method. - ``totalBytes`` — total size of useful data on the file system - -``usedBytes`` — number of bytes used by files - ``blockSize`` — SPIFFS -block size - ``pageSize`` — SPIFFS logical page size - ``maxOpenFiles`` +``usedBytes`` — number of bytes used by files - ``blockSize`` — filesystem +block size - ``pageSize`` — filesystem logical page size - ``maxOpenFiles`` — max number of files which may be open simultaneously - ``maxPathLength`` — max file name length (including one byte for zero termination) @@ -304,14 +406,14 @@ Directory object (Dir) ---------------------- The purpose of *Dir* object is to iterate over files inside a directory. -It provides the methods: ``next()``, ``fileName()``, ``fileSize()`` , and -``openFile(mode)``. +It provides multiple access methods. The following example shows how it should be used: .. code:: cpp Dir dir = SPIFFS.openDir("/data"); + // or Dir dir = LittleFS.openDir("/data"); while (dir.next()) { Serial.print(dir.fileName()); if(dir.fileSize()) { @@ -339,16 +441,33 @@ fileSize Returns the size of the current file pointed to by the internal iterator. +isFile +~~~~~~ + +Returns *true* if the current file pointed to by +the internal iterator is a File. + +isDirectory +~~~~~~~~~~~ + +Returns *true* if the current file pointed to by +the internal iterator is a Directory. + openFile ~~~~~~~~ This method takes *mode* argument which has the same meaning as -for ``SPIFFS.open()`` function. +for ``SPIFFS/LittleFS.open()`` function. + +rewind +~~~~~~ + +Resets the internal pointer to the start of the directory. File object ----------- -``SPIFFS.open()`` and ``dir.openFile()`` functions return a *File* object. +``SPIFFS/LittleFS.open()`` and ``dir.openFile()`` functions return a *File* object. This object supports all the functions of *Stream*, so you can use ``readBytes``, ``findUntil``, ``parseInt``, ``println``, and all other *Stream* methods. @@ -399,9 +518,42 @@ name String name = file.name(); -Returns file name, as ``const char*``. Convert it to *String* for +Returns short (no-path) file name, as ``const char*``. Convert it to *String* for storage. +fullName +~~~~~~~~ + +.. code:: cpp + + // Filesystem: + // testdir/ + // file1 + Dir d = LittleFS.openDir("testdir/"); + File f = d.openFile("r"); + // f.name() == "file1", f.fullName() == "testdir/file1" + +Returns the full path file name as a ``const char*``. + +isFile +~~~~~~ + +.. code:: cpp + + bool amIAFile = file.isFile(); + +Returns *true* if this File points to a real file. + +isDirectory +~~~~~~~~~~~ + +.. code:: cpp + + bool amIADir = file.isDir(); + +Returns *true* if this File points to a directory (used for emulation +of the SD.* interfaces with the ``openNextFile`` method). + close ~~~~~ @@ -411,3 +563,29 @@ close Close the file. No other operations should be performed on *File* object after ``close`` function was called. + +openNextFile (compatibiity method, not recommended for new code) +~~~~~~~~~~~~ + +.. code:: cpp + + File root = LittleFS.open("/"); + File file1 = root.openNextFile(); + File files = root.openNextFile(); + +Opens the next file in the directory pointed to by the File. Only valid +when ``File.isDirectory() == true``. + +rewindDirectory (compatibiity method, not recommended for new code) +~~~~~~~~~~~~~~~ + +.. code:: cpp + + File root = LittleFS.open("/"); + File file1 = root.openNextFile(); + file1.close(); + root.rewindDirectory(); + file1 = root.openNextFile(); // Opens first file in dir again + +Resets the ``openNextFile`` pointer to the top of the directory. Only +valid when ``File.isDirectory() == true``. diff --git a/libraries/ArduinoOTA/ArduinoOTA.cpp b/libraries/ArduinoOTA/ArduinoOTA.cpp index ea619d4562..9429518d3f 100644 --- a/libraries/ArduinoOTA/ArduinoOTA.cpp +++ b/libraries/ArduinoOTA/ArduinoOTA.cpp @@ -186,7 +186,7 @@ void ArduinoOTAClass::_onRx(){ if (_state == OTA_IDLE) { int cmd = parseInt(); - if (cmd != U_FLASH && cmd != U_SPIFFS) + if (cmd != U_FLASH && cmd != U_FS) return; _ota_ip = _udp_ota->getRemoteAddress(); _cmd = cmd; diff --git a/libraries/ArduinoOTA/ArduinoOTA.h b/libraries/ArduinoOTA/ArduinoOTA.h index a02dbc485c..1dfcaeed38 100644 --- a/libraries/ArduinoOTA/ArduinoOTA.h +++ b/libraries/ArduinoOTA/ArduinoOTA.h @@ -65,7 +65,7 @@ class ArduinoOTAClass //Call this in loop() to run the service. Also calls MDNS.update() when begin() or begin(true) is used. void handle(); - //Gets update command type after OTA has started. Either U_FLASH or U_SPIFFS + //Gets update command type after OTA has started. Either U_FLASH or U_FS int getCommand(); private: diff --git a/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino b/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino index 214eb82952..ddc5629741 100644 --- a/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino +++ b/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino @@ -39,11 +39,11 @@ void setup() { String type; if (ArduinoOTA.getCommand() == U_FLASH) { type = "sketch"; - } else { // U_SPIFFS + } else { // U_FS type = "filesystem"; } - // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + // NOTE: if updating FS this would be the place to unmount FS using FS.end() Serial.println("Start updating " + type); }); ArduinoOTA.onEnd([]() { diff --git a/libraries/EEPROM/EEPROM.cpp b/libraries/EEPROM/EEPROM.cpp index 0ec376abc1..fa1aa3ee06 100644 --- a/libraries/EEPROM/EEPROM.cpp +++ b/libraries/EEPROM/EEPROM.cpp @@ -30,7 +30,7 @@ extern "C" { #include "spi_flash.h" } -extern "C" uint32_t _SPIFFS_end; +extern "C" uint32_t _FS_end; EEPROMClass::EEPROMClass(uint32_t sector) : _sector(sector) @@ -41,7 +41,7 @@ EEPROMClass::EEPROMClass(uint32_t sector) } EEPROMClass::EEPROMClass(void) -: _sector((((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE)) +: _sector((((uint32_t)&_FS_end - 0x40200000) / SPI_FLASH_SEC_SIZE)) , _data(0) , _size(0) , _dirty(false) diff --git a/libraries/ESP8266SdFat b/libraries/ESP8266SdFat index f096292195..2499d4e0c6 160000 --- a/libraries/ESP8266SdFat +++ b/libraries/ESP8266SdFat @@ -1 +1 @@ -Subproject commit f0962921955e2503243f28de0fdc9ac188177ffb +Subproject commit 2499d4e0c60db1a67a47532fc44c17d2d5116e4c diff --git a/libraries/ESP8266httpUpdate/src/ESP8266httpUpdate.cpp b/libraries/ESP8266httpUpdate/src/ESP8266httpUpdate.cpp index bc599f84bf..d3029c453e 100644 --- a/libraries/ESP8266httpUpdate/src/ESP8266httpUpdate.cpp +++ b/libraries/ESP8266httpUpdate/src/ESP8266httpUpdate.cpp @@ -26,8 +26,8 @@ #include "ESP8266httpUpdate.h" #include -extern "C" uint32_t _SPIFFS_start; -extern "C" uint32_t _SPIFFS_end; +extern "C" uint32_t _FS_start; +extern "C" uint32_t _FS_end; ESP8266HTTPUpdate::ESP8266HTTPUpdate(void) : _httpClientTimeout(8000), _followRedirects(false), _ledPin(-1) @@ -321,7 +321,7 @@ HTTPUpdateResult ESP8266HTTPUpdate::handleUpdate(HTTPClient& http, const String& if(len > 0) { bool startUpdate = true; if(spiffs) { - size_t spiffsSize = ((size_t) &_SPIFFS_end - (size_t) &_SPIFFS_start); + size_t spiffsSize = ((size_t) &_FS_end - (size_t) &_FS_start); if(len > (int) spiffsSize) { DEBUG_HTTP_UPDATE("[httpUpdate] spiffsSize to low (%d) needed: %d\n", spiffsSize, len); startUpdate = false; @@ -348,8 +348,8 @@ HTTPUpdateResult ESP8266HTTPUpdate::handleUpdate(HTTPClient& http, const String& int command; if(spiffs) { - command = U_SPIFFS; - DEBUG_HTTP_UPDATE("[httpUpdate] runUpdate spiffs...\n"); + command = U_FS; + DEBUG_HTTP_UPDATE("[httpUpdate] runUpdate filesystem...\n"); } else { command = U_FLASH; DEBUG_HTTP_UPDATE("[httpUpdate] runUpdate flash...\n"); diff --git a/libraries/LittleFS/examples/SpeedTest/SpeedTest.ino b/libraries/LittleFS/examples/SpeedTest/SpeedTest.ino new file mode 100644 index 0000000000..5b7dd453a0 --- /dev/null +++ b/libraries/LittleFS/examples/SpeedTest/SpeedTest.ino @@ -0,0 +1,125 @@ +// Simple speed test for filesystem objects +// Released to the public domain by Earle F. Philhower, III + +#include + +#define TESTSIZEKB 512 + +void DoTest(FS *fs) { + if (!fs->format()) { + Serial.printf("Unable to format(), aborting\n"); + return; + } + if (!fs->begin()) { + Serial.printf("Unable to begin(), aborting\n"); + return; + } + + uint8_t data[256]; + for (int i = 0; i < 256; i++) { + data[i] = (uint8_t) i; + } + + Serial.printf("Creating %dKB file, may take a while...\n", TESTSIZEKB); + long start = millis(); + File f = fs->open("/testwrite.bin", "w"); + if (!f) { + Serial.printf("Unable to open file for writing, aborting\n"); + return; + } + for (int i = 0; i < TESTSIZEKB; i++) { + for (int j = 0; j < 4; j++) { + f.write(data, 256); + } + } + f.close(); + long stop = millis(); + Serial.printf("==> Time to write %dKB in 256b chunks = %ld milliseconds\n", TESTSIZEKB, stop - start); + + f = fs->open("/testwrite.bin", "r"); + Serial.printf("==> Created file size = %d\n", f.size()); + f.close(); + + Serial.printf("Reading %dKB file sequentially in 256b chunks\n", TESTSIZEKB); + start = millis(); + f = fs->open("/testwrite.bin", "r"); + for (int i = 0; i < TESTSIZEKB; i++) { + for (int j = 0; j < 4; j++) { + f.read(data, 256); + } + } + f.close(); + stop = millis(); + Serial.printf("==> Time to read %dKB sequentially in 256b chunks = %ld milliseconds = %ld bytes/s\n", TESTSIZEKB, stop - start, TESTSIZEKB * 1024 / (stop - start) * 1000); + + Serial.printf("Reading %dKB file MISALIGNED in flash and RAM sequentially in 256b chunks\n", TESTSIZEKB); + start = millis(); + f = fs->open("/testwrite.bin", "r"); + f.read(); + for (int i = 0; i < TESTSIZEKB; i++) { + for (int j = 0; j < 4; j++) { + f.read(data + 1, 256); + } + } + f.close(); + stop = millis(); + Serial.printf("==> Time to read %dKB sequentially MISALIGNED in flash and RAM in 256b chunks = %ld milliseconds = %ld bytes/s\n", TESTSIZEKB, stop - start, TESTSIZEKB * 1024 / (stop - start) * 1000); + + + Serial.printf("Reading %dKB file in reverse by 256b chunks\n", TESTSIZEKB); + start = millis(); + f = fs->open("/testwrite.bin", "r"); + for (int i = 0; i < TESTSIZEKB; i++) { + for (int j = 0; j < 4; j++) { + if (!f.seek(256 + 256 * j * i, SeekEnd)) { + Serial.printf("Unable to seek to %d, aborting\n", -256 - 256 * j * i); + return; + } + if (256 != f.read(data, 256)) { + Serial.printf("Unable to read 256 bytes, aborting\n"); + return; + } + } + } + f.close(); + stop = millis(); + Serial.printf("==> Time to read %dKB in reverse in 256b chunks = %ld milliseconds = %ld bytes/s\n", TESTSIZEKB, stop - start, TESTSIZEKB * 1024 / (stop - start) * 1000); + + + Serial.printf("Writing 64K file in 1-byte chunks\n"); + start = millis(); + f = fs->open("/test1b.bin", "w"); + for (int i = 0; i < 65536; i++) { + f.write((uint8_t*)&i, 1); + } + f.close(); + stop = millis(); + Serial.printf("==> Time to write 64KB in 1b chunks = %ld milliseconds = %ld bytes/s\n", stop - start, 65536 / (stop - start) * 1000); + + Serial.printf("Reading 64K file in 1-byte chunks\n"); + start = millis(); + f = fs->open("/test1b.bin", "r"); + for (int i = 0; i < 65536; i++) { + char c; + f.read((uint8_t*)&c, 1); + } + f.close(); + stop = millis(); + Serial.printf("==> Time to read 64KB in 1b chunks = %ld milliseconds = %ld bytes/s\n", stop - start, 65536 / (stop - start) * 1000); + + +} + +void setup() { + Serial.begin(115200); + Serial.printf("Beginning LittleFS test\n"); + Serial.flush(); + DoTest(&LittleFS); + Serial.printf("Beginning SPIFFS test\n"); + Serial.flush(); + DoTest(&SPIFFS); +} + +void loop() { + delay(10000); +} diff --git a/libraries/LittleFS/lib/littlefs b/libraries/LittleFS/lib/littlefs new file mode 160000 index 0000000000..f35fb8c148 --- /dev/null +++ b/libraries/LittleFS/lib/littlefs @@ -0,0 +1 @@ +Subproject commit f35fb8c14866a4a4677756f6dbeca78f8a9b4001 diff --git a/libraries/LittleFS/library.properties b/libraries/LittleFS/library.properties new file mode 100644 index 0000000000..a13e6eb755 --- /dev/null +++ b/libraries/LittleFS/library.properties @@ -0,0 +1,10 @@ +name=LittleFS(esp8266) +version=0.1.0 +author=Earle F. Philhower, III +maintainer=Earle F. Philhower, III +sentence=Port of LittleFS to ESP8266 Arduino +paragraph=Replacement for SPIFFS to manage a filesystem in the onboard flash, supporting power fail safety and higher performance than SPIFFS at the cost of a lower maximum number of files. +category=Data Storage +url=https://github.com/esp8266/Arduino/libraries/LittleFS +architectures=esp8266 +dot_a_linkage=true diff --git a/libraries/LittleFS/src/LittleFS.cpp b/libraries/LittleFS/src/LittleFS.cpp new file mode 100644 index 0000000000..b4aba1970a --- /dev/null +++ b/libraries/LittleFS/src/LittleFS.cpp @@ -0,0 +1,198 @@ +/* + LittleFS.cpp - Wrapper for LittleFS for ESP8266 + Copyright (c_ 2019 Earle F. Philhower, III. All rights reserved. + + Based extensively off of the ESP8266 SPIFFS code, which is + Copyright (c) 2015 Ivan Grokhotkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include "LittleFS.h" +#include "debug.h" +#include "flash_hal.h" + +extern "C" { +#include "c_types.h" +#include "spi_flash.h" +} + +namespace littlefs_impl { + +FileImplPtr LittleFSImpl::open(const char* path, OpenMode openMode, AccessMode accessMode) { + if (!_mounted) { + DEBUGV("LittleFSImpl::open() called on unmounted FS\n"); + return FileImplPtr(); + } + if (!path || !path[0]) { + DEBUGV("LittleFSImpl::open() called with invalid filename\n"); + return FileImplPtr(); + } + if (!LittleFSImpl::pathValid(path)) { + DEBUGV("LittleFSImpl::open() called with too long filename\n"); + return FileImplPtr(); + } + + int flags = _getFlags(openMode, accessMode); + auto fd = std::make_shared(); + + if ((openMode && OM_CREATE) && strchr(path, '/')) { + // For file creation, silently make subdirs as needed. If any fail, + // it will be caught by the real file open later on + char *pathStr = strdup(path); + if (pathStr) { + // Make dirs up to the final fnamepart + char *ptr = strchr(pathStr, '/'); + while (ptr) { + *ptr = 0; + lfs_mkdir(&_lfs, pathStr); + *ptr = '/'; + ptr = strchr(ptr+1, '/'); + } + } + free(pathStr); + } + int rc = lfs_file_open(&_lfs, fd.get(), path, flags); + if (rc == LFS_ERR_ISDIR) { + // To support the SD.openNextFile, a null FD indicates to the LittleFSFile this is just + // a directory whose name we are carrying around but which cannot be read or written + return std::make_shared(this, path, nullptr); + } else if (rc == 0) { + return std::make_shared(this, path, fd); + } else { + DEBUGV("LittleFSDirImpl::openFile: rc=%d fd=%p path=`%s` openMode=%d accessMode=%d err=%d\n", + rc, fd.get(), path, openMode, accessMode, rc); + return FileImplPtr(); + } +} + +DirImplPtr LittleFSImpl::openDir(const char *path) { + if (!_mounted || !path) { + return DirImplPtr(); + } + char *pathStr = strdup(path); // Allow edits on our scratch copy + // Get rid of any trailing slashes + while (strlen(pathStr) && (pathStr[strlen(pathStr)-1]=='/')) { + pathStr[strlen(pathStr)-1] = 0; + } + // At this point we have a name of "blah/blah/blah" or "blah" or "" + // If that references a directory, just open it and we're done. + lfs_info info; + auto dir = std::make_shared(); + int rc; + const char *filter = ""; + if (!pathStr[0]) { + // openDir("") === openDir("/") + rc = lfs_dir_open(&_lfs, dir.get(), "/"); + filter = ""; + } else if (lfs_stat(&_lfs, pathStr, &info) >= 0) { + if (info.type == LFS_TYPE_DIR) { + // Easy peasy, path specifies an existing dir! + rc = lfs_dir_open(&_lfs, dir.get(), pathStr); + filter = ""; + } else { + // This is a file, so open the containing dir + char *ptr = strrchr(pathStr, '/'); + if (!ptr) { + // No slashes, open the root dir + rc = lfs_dir_open(&_lfs, dir.get(), "/"); + filter = pathStr; + } else { + // We've got slashes, open the dir one up + *ptr = 0; // Remove slash, truncate string + rc = lfs_dir_open(&_lfs, dir.get(), pathStr); + filter = ptr + 1; + } + } + } else { + // Name doesn't exist, so use the parent dir of whatever was sent in + // This is a file, so open the containing dir + char *ptr = strrchr(pathStr, '/'); + if (!ptr) { + // No slashes, open the root dir + rc = lfs_dir_open(&_lfs, dir.get(), "/"); + filter = pathStr; + } else { + // We've got slashes, open the dir one up + *ptr = 0; // Remove slash, truncate string + rc = lfs_dir_open(&_lfs, dir.get(), pathStr); + filter = ptr + 1; + } + } + if (rc < 0) { + DEBUGV("LittleFSImpl::openDir: path=`%s` err=%d\n", path, rc); + free(pathStr); + return DirImplPtr(); + } + // Skip the . and .. entries + lfs_info dirent; + lfs_dir_read(&_lfs, dir.get(), &dirent); + lfs_dir_read(&_lfs, dir.get(), &dirent); + + auto ret = std::make_shared(filter, this, dir, pathStr); + free(pathStr); + return ret; +} + +int LittleFSImpl::lfs_flash_read(const struct lfs_config *c, + lfs_block_t block, lfs_off_t off, void *dst, lfs_size_t size) { + LittleFSImpl *me = reinterpret_cast(c->context); + uint32_t addr = me->_start + (block * me->_blockSize) + off; + return flash_hal_read(addr, size, static_cast(dst)) == FLASH_HAL_OK ? 0 : -1; +} + +int LittleFSImpl::lfs_flash_prog(const struct lfs_config *c, + lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { + LittleFSImpl *me = reinterpret_cast(c->context); + uint32_t addr = me->_start + (block * me->_blockSize) + off; + const uint8_t *src = reinterpret_cast(buffer); + return flash_hal_write(addr, size, static_cast(src)) == FLASH_HAL_OK ? 0 : -1; +} + +int LittleFSImpl::lfs_flash_erase(const struct lfs_config *c, lfs_block_t block) { + LittleFSImpl *me = reinterpret_cast(c->context); + uint32_t addr = me->_start + (block * me->_blockSize); + uint32_t size = me->_blockSize; + return flash_hal_erase(addr, size) == FLASH_HAL_OK ? 0 : -1; +} + +int LittleFSImpl::lfs_flash_sync(const struct lfs_config *c) { + /* NOOP */ + (void) c; + return 0; +} + + +}; // namespace + +// these symbols should be defined in the linker script for each flash layout +#ifndef CORE_MOCK +#ifdef ARDUINO +#ifndef FS_MAX_OPEN_FILES +#define FS_MAX_OPEN_FILES 5 +#endif + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_LITTLEFS) +FS LittleFS = FS(FSImplPtr(new littlefs_impl::LittleFSImpl(FS_PHYS_ADDR, FS_PHYS_SIZE, FS_PHYS_PAGE, FS_PHYS_BLOCK, FS_MAX_OPEN_FILES))); +#endif + +#endif // !CORE_MOCK + + +#endif diff --git a/libraries/LittleFS/src/LittleFS.h b/libraries/LittleFS/src/LittleFS.h new file mode 100644 index 0000000000..f81541557c --- /dev/null +++ b/libraries/LittleFS/src/LittleFS.h @@ -0,0 +1,557 @@ +/* + LittleFS.h - Filesystem wrapper for LittleFS on the ESP8266 + Copyright (c) 2019 Earle F. Philhower, III. All rights reserved. + + Based heavily off of the SPIFFS equivalent code in the ESP8266 core + "Copyright (c) 2015 Ivan Grokhotkov. All rights reserved." + + This code was influenced by NodeMCU and Sming libraries, and first version of + Arduino wrapper written by Hristo Gochkov. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#ifndef __LITTLEFS_H +#define __LITTLEFS_H + +#include +#include +#include +#include +#include +#include + +#define LFS_NAME_MAX 32 +#include "../lib/littlefs/lfs.h" + +using namespace fs; + +namespace littlefs_impl { + +class LittleFSFileImpl; +class LittleFSDirImpl; + +class LittleFSConfig : public FSConfig +{ +public: + LittleFSConfig(bool autoFormat = true) { + _type = LittleFSConfig::fsid::FSId; + _autoFormat = autoFormat; + } + enum fsid { FSId = 0x4c495454 }; +}; + +class LittleFSImpl : public FSImpl +{ +public: + LittleFSImpl(uint32_t start, uint32_t size, uint32_t pageSize, uint32_t blockSize, uint32_t maxOpenFds) + : _start(start) , _size(size) , _pageSize(pageSize) , _blockSize(blockSize) , _maxOpenFds(maxOpenFds), + _mounted(false) { + memset(&_lfs, 0, sizeof(_lfs)); + memset(&_lfs_cfg, 0, sizeof(_lfs_cfg)); + _lfs_cfg.context = (void*) this; + _lfs_cfg.read = lfs_flash_read; + _lfs_cfg.prog = lfs_flash_prog; + _lfs_cfg.erase = lfs_flash_erase; + _lfs_cfg.sync = lfs_flash_sync; + _lfs_cfg.read_size = 64; + _lfs_cfg.prog_size = 64; + _lfs_cfg.block_size = _blockSize; + _lfs_cfg.block_count = _size / _blockSize; + _lfs_cfg.block_cycles = 16; // TODO - need better explanation + _lfs_cfg.cache_size = 64; + _lfs_cfg.lookahead_size = 64; + _lfs_cfg.read_buffer = nullptr; + _lfs_cfg.prog_buffer = nullptr; + _lfs_cfg.lookahead_buffer = nullptr; + _lfs_cfg.name_max = 0; + _lfs_cfg.file_max = 0; + _lfs_cfg.attr_max = 0; + } + + ~LittleFSImpl() { + if (_mounted) { + lfs_unmount(&_lfs); + } + } + + FileImplPtr open(const char* path, OpenMode openMode, AccessMode accessMode) override; + DirImplPtr openDir(const char *path) override; + + bool exists(const char* path) override { + if ( !_mounted || !path || !path[0] ) { + return false; + } + lfs_info info; + int rc = lfs_stat(&_lfs, path, &info); + return rc == 0; + } + + bool rename(const char* pathFrom, const char* pathTo) override { + if (!_mounted || !pathFrom || !pathFrom[0] || !pathTo || !pathTo[0]) { + return false; + } + int rc = lfs_rename(&_lfs, pathFrom, pathTo); + if (rc != 0) { + DEBUGV("lfs_rename: rc=%d, from=`%s`, to=`%s`\n", rc, pathFrom, pathTo); + return false; + } + return true; + } + + bool info(FSInfo& info) override { + if (!_mounted) { + return false; + } + info.maxOpenFiles = _maxOpenFds; + info.blockSize = _blockSize; + info.pageSize = _pageSize; + info.maxOpenFiles = _maxOpenFds; + info.maxPathLength = LFS_NAME_MAX; + info.totalBytes = _size; + info.usedBytes = _getUsedBlocks() * _blockSize; + return true; + } + + bool remove(const char* path) override { + if (!_mounted || !path || !path[0]) { + return false; + } + int rc = lfs_remove(&_lfs, path); + if (rc != 0) { + DEBUGV("lfs_remove: rc=%d path=`%s`\n", rc, path); + return false; + } + // Now try and remove any empty subdirs this makes, silently + char *pathStr = strdup(path); + if (pathStr) { + char *ptr = strrchr(pathStr, '/'); + while (ptr) { + *ptr = 0; + lfs_remove(&_lfs, pathStr); // Don't care if fails if there are files left + ptr = strrchr(pathStr, '/'); + } + free(pathStr); + } + return true; + } + + bool mkdir(const char* path) override { + if (!_mounted || !path || !path[0]) { + return false; + } + int rc = lfs_mkdir(&_lfs, path); + return (rc==0); + } + + bool rmdir(const char* path) override { + return remove(path); // Same call on LittleFS + } + + bool setConfig(const FSConfig &cfg) override { + if ((cfg._type != LittleFSConfig::fsid::FSId) || _mounted) { + return false; + } + _cfg = *static_cast(&cfg); + return true; + } + + bool begin() override { + if (_size <= 0) { + DEBUGV("LittleFS size is <= zero"); + return false; + } + if (_tryMount()) { + return true; + } + if (!_cfg._autoFormat || !format()) { + return false; + } + return _tryMount(); + } + + void end() override { + if (!_mounted) { + return; + } + lfs_unmount(&_lfs); + _mounted = false; + } + + bool format() override { + if (_size == 0) { + DEBUGV("lfs size is zero\n"); + return false; + } + + bool wasMounted = _mounted; + if (_mounted) { + lfs_unmount(&_lfs); + _mounted = false; + } + + memset(&_lfs, 0, sizeof(_lfs)); + int rc = lfs_format(&_lfs, &_lfs_cfg); + if (rc != 0) { + DEBUGV("lfs_format: rc=%d\n", rc); + return false; + } + + if (wasMounted) { + return _tryMount(); + } + + return true; + } + + +protected: + friend class LittleFSFileImpl; + friend class LittleFSDirImpl; + + lfs_t* getFS() { + return &_lfs; + } + + bool _tryMount() { + if (_mounted) { + lfs_unmount(&_lfs); + _mounted = false; + } + memset(&_lfs, 0, sizeof(_lfs)); + int rc = lfs_mount(&_lfs, &_lfs_cfg); + if (rc==0) { + _mounted = true; + } + return _mounted; + } + + int _getUsedBlocks() { + if (!_mounted) { + return 0; + } + return lfs_fs_size(&_lfs); + } + + static int _getFlags(OpenMode openMode, AccessMode accessMode) { + int mode = 0; + if (openMode & OM_CREATE) { + mode |= LFS_O_CREAT; + } + if (openMode & OM_APPEND) { + mode |= LFS_O_APPEND; + } + if (openMode & OM_TRUNCATE) { + mode |= LFS_O_TRUNC; + } + if (accessMode & AM_READ) { + mode |= LFS_O_RDONLY; + } + if (accessMode & AM_WRITE) { + mode |= LFS_O_WRONLY; + } + return mode; + } + + // Check that no components of path beyond max len + static bool pathValid(const char *path) { + while (*path) { + const char *slash = strchr(path, '/'); + if (!slash) { + if (strlen(path) >= LFS_NAME_MAX) { + // Terminal filename is too long + return false; + } + break; + } + if ((slash - path) >= LFS_NAME_MAX) { + // This subdir name too long + return false; + } + path = slash + 1; + } + return true; + } + + // The actual flash accessing routines + static int lfs_flash_read(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size); + static int lfs_flash_prog(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size); + static int lfs_flash_erase(const struct lfs_config *c, lfs_block_t block); + static int lfs_flash_sync(const struct lfs_config *c); + + lfs_t _lfs; + lfs_config _lfs_cfg; + + LittleFSConfig _cfg; + + uint32_t _start; + uint32_t _size; + uint32_t _pageSize; + uint32_t _blockSize; + uint32_t _maxOpenFds; + + bool _mounted; +}; + + +class LittleFSFileImpl : public FileImpl +{ +public: + LittleFSFileImpl(LittleFSImpl* fs, const char *name, std::shared_ptr fd) : _fs(fs), _fd(fd), _opened(true) { + _name = std::shared_ptr(new char[strlen(name) + 1], std::default_delete()); + strcpy(_name.get(), name); + } + + ~LittleFSFileImpl() override { + if (_opened) { + close(); + } + } + + size_t write(const uint8_t *buf, size_t size) override { + if (!_opened || !_fd || !buf) { + return 0; + } + int result = lfs_file_write(_fs->getFS(), _getFD(), (void*) buf, size); + if (result < 0) { + DEBUGV("lfs_write rc=%d\n", result); + return 0; + } + return result; + } + + size_t read(uint8_t* buf, size_t size) override { + if (!_opened || !_fd | !buf) { + return 0; + } + int result = lfs_file_read(_fs->getFS(), _getFD(), (void*) buf, size); + if (result < 0) { + DEBUGV("lfs_read rc=%d\n", result); + return 0; + } + + return result; + } + + void flush() override { + if (!_opened || !_fd) { + return; + } + int rc = lfs_file_sync(_fs->getFS(), _getFD()); + if (rc < 0) { + DEBUGV("lfs_file_sync rc=%d\n", rc); + } + } + + bool seek(uint32_t pos, SeekMode mode) override { + if (!_opened || !_fd) { + return false; + } + int32_t offset = static_cast(pos); + if (mode == SeekEnd) { + offset = -offset; // TODO - this seems like its plain wrong vs. POSIX + } + int rc = lfs_file_seek(_fs->getFS(), _getFD(), offset, (int)mode); // NB. SeekMode === LFS_SEEK_TYPES + if (rc < 0) { + DEBUGV("lfs_file_seek rc=%d\n", rc); + return false; + } + return true; + } + + size_t position() const override { + if (!_opened || !_fd) { + return 0; + } + int result = lfs_file_tell(_fs->getFS(), _getFD()); + if (result < 0) { + DEBUGV("lfs_file_tell rc=%d\n", result); + return 0; + } + + return result; + } + + size_t size() const override { + return (_opened && _fd)? lfs_file_size(_fs->getFS(), _getFD()) : 0; + } + + bool truncate(uint32_t size) override { + if (!_opened || !_fd) { + return false; + } + int rc = lfs_file_truncate(_fs->getFS(), _getFD(), size); + if (rc < 0) { + DEBUGV("lfs_file_truncate rc=%d\n", rc); + return false; + } + return true; + } + + void close() override { + if (_opened && _fd) { + lfs_file_close(_fs->getFS(), _getFD()); + _opened = false; + DEBUGV("lfs_file_close: fd=%p\n", _getFD()); + } + } + + const char* name() const override { + if (!_opened) { + return nullptr; + } else { + const char *p = _name.get(); + const char *slash = strrchr(p, '/'); + return (slash && slash[1]) ? slash + 1 : p; + } + } + + const char* fullName() const override { + return _opened ? _name.get() : nullptr; + } + + bool isFile() const override { + if (!_opened || !_fd) { + return false; + } + lfs_info info; + int rc = lfs_stat(_fs->getFS(), fullName(), &info); + return (rc == 0) && (info.type == LFS_TYPE_REG); + } + + bool isDirectory() const override { + if (!_opened) { + return false; + } else if (!_fd) { + return true; + } + lfs_info info; + int rc = lfs_stat(_fs->getFS(), fullName(), &info); + return (rc == 0) && (info.type == LFS_TYPE_DIR); + } + +protected: + lfs_file_t *_getFD() const { + return _fd.get(); + } + + LittleFSImpl *_fs; + std::shared_ptr _fd; + std::shared_ptr _name; + bool _opened; +}; + +class LittleFSDirImpl : public DirImpl +{ +public: + LittleFSDirImpl(const String& pattern, LittleFSImpl* fs, std::shared_ptr dir, const char *dirPath = nullptr) + : _pattern(pattern) , _fs(fs) , _dir(dir) , _dirPath(nullptr), _valid(false), _opened(true) + { + memset(&_dirent, 0, sizeof(_dirent)); + if (dirPath) { + _dirPath = std::shared_ptr(new char[strlen(dirPath) + 1], std::default_delete()); + strcpy(_dirPath.get(), dirPath); + } + } + + ~LittleFSDirImpl() override { + if (_opened) { + lfs_dir_close(_fs->getFS(), _getDir()); + } + } + + FileImplPtr openFile(OpenMode openMode, AccessMode accessMode) override { + if (!_valid) { + return FileImplPtr(); + } + int nameLen = 3; // Slashes, terminator + nameLen += _dirPath.get() ? strlen(_dirPath.get()) : 0; + nameLen += strlen(_dirent.name); + char *tmpName = (char*)malloc(nameLen); + if (!tmpName) { + return FileImplPtr(); + } + snprintf(tmpName, nameLen, "%s%s%s", _dirPath.get() ? _dirPath.get() : "", _dirPath.get()&&_dirPath.get()[0]?"/":"", _dirent.name); + auto ret = _fs->open((const char *)tmpName, openMode, accessMode); + free(tmpName); + return ret; + } + + const char* fileName() override { + if (!_valid) { + return nullptr; + } + return (const char*) _dirent.name; + } + + size_t fileSize() override { + if (!_valid) { + return 0; + } + return _dirent.size; + } + + bool isFile() const override { + return _valid && (_dirent.type == LFS_TYPE_REG); + } + + bool isDirectory() const override { + return _valid && (_dirent.type == LFS_TYPE_DIR); + } + + bool rewind() override { + _valid = false; + int rc = lfs_dir_rewind(_fs->getFS(), _getDir()); + return (rc == 0); + } + + bool next() override { + const int n = _pattern.length(); + bool match; + do { + _dirent.name[0] = 0; + int rc = lfs_dir_read(_fs->getFS(), _getDir(), &_dirent); + _valid = (rc == 1); + match = (!n || !strncmp((const char*) _dirent.name, _pattern.c_str(), n)); + } while (_valid && !match); + return _valid; + } + +protected: + lfs_dir_t *_getDir() const { + return _dir.get(); + } + + String _pattern; + LittleFSImpl *_fs; + std::shared_ptr _dir; + std::shared_ptr _dirPath; + lfs_info _dirent; + bool _valid; + bool _opened; +}; + +}; + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_LITTLEFS) +extern FS LittleFS; +using littlefs_impl::LittleFSConfig; +#endif // ARDUINO + + +#endif // !defined(__LITTLEFS_H) diff --git a/libraries/LittleFS/src/lfs.c b/libraries/LittleFS/src/lfs.c new file mode 100644 index 0000000000..62022fc375 --- /dev/null +++ b/libraries/LittleFS/src/lfs.c @@ -0,0 +1,10 @@ +// Can't place library in ths src/ directory, Arduino will attempt to build the tests/etc. +// Just have a stub here that redirects to the actual source file + +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#define LFS_NAME_MAX 32 +#define LFS_NO_DEBUG +#define LFS_NO_WARN +#define LFS_NO_ERROR + +#include "../lib/littlefs/lfs.c" diff --git a/libraries/LittleFS/src/lfs_util.c b/libraries/LittleFS/src/lfs_util.c new file mode 100644 index 0000000000..43ada31207 --- /dev/null +++ b/libraries/LittleFS/src/lfs_util.c @@ -0,0 +1,6 @@ +#define LFS_NAME_MAX 32 +#define LFS_NO_DEBUG +#define LFS_NO_WARN +#define LFS_NO_ERROR + +#include "../lib/littlefs/lfs_util.c" diff --git a/libraries/SD/src/SD.h b/libraries/SD/src/SD.h index fd4b61e889..bb1c696a58 100644 --- a/libraries/SD/src/SD.h +++ b/libraries/SD/src/SD.h @@ -47,10 +47,18 @@ class SDClass { return SDFS.open(filename, getMode(mode)); } + File open(const char *filename, const char *mode) { + return SDFS.open(filename, mode); + } + File open(const String &filename, uint8_t mode = FILE_READ) { return open(filename.c_str(), mode); } + File open(const String &filename, const char *mode) { + return open(filename.c_str(), mode); + } + boolean exists(const char *filepath) { return (boolean)SDFS.exists(filepath); } diff --git a/package/package_esp8266com_index.template.json b/package/package_esp8266com_index.template.json index 2d08d116aa..3b5c58847c 100644 --- a/package/package_esp8266com_index.template.json +++ b/package/package_esp8266com_index.template.json @@ -119,6 +119,11 @@ "version": "2.5.0-3-20ed2b9", "name": "mkspiffs" }, + { + "packager": "esp8266", + "version": "2.5.1-1", + "name": "mklittlefs" + }, { "packager": "esp8266", "version": "3.7.2-post1", @@ -295,6 +300,54 @@ "size": "350035" } ] + }, + { + "version": "2.5.1-1", + "name": "mklittlefs", + "systems": [ + { + "host": "aarch64-linux-gnu", + "url": "https://github.com/earlephilhower/mklittlefs/releases/download/2.5.1-1/aarch64-linux-gnu-mklittlefs-47e8167.tar.gz", + "archiveFileName": "aarch64-linux-gnu-mklittlefs-47e8167.tar.gz", + "checksum": "SHA-256:f5951f0f5c0649992cccbed0146e5ec91dd0a2294f391956bf65c32404a98e96", + "size": "44128" + }, + { + "host": "arm-linux-gnueabihf", + "url": "https://github.com/earlephilhower/mklittlefs/releases/download/2.5.1-1/arm-linux-gnueabihf-mklittlefs-47e8167.tar.gz", + "archiveFileName": "arm-linux-gnueabihf-mklittlefs-47e8167.tar.gz", + "checksum": "SHA-256:c5252ce0ae177238efecfa6835950ae12b3cb9dbb7f6d04caedf90afcf0b27b6", + "size": "36590" + }, + { + "host": "i686-mingw32", + "url": "https://github.com/earlephilhower/mklittlefs/releases/download/2.5.1-1/i686-w64-mingw32-mklittlefs-47e8167.zip", + "archiveFileName": "i686-w64-mingw32-mklittlefs-47e8167.zip", + "checksum": "SHA-256:85b2dccc9c00ab2747ffbcee8b949c2dc88747f7a7e14cd0bac5b8a8afe19a4b", + "size": "332150" + }, + { + "host": "x86_64-apple-darwin", + "url": "https://github.com/earlephilhower/mklittlefs/releases/download/2.5.1-1/x86_64-apple-darwin14-mklittlefs-47e8167.tar.gz", + "archiveFileName": "x86_64-apple-darwin14-mklittlefs-47e8167.tar.gz", + "checksum": "SHA-256:47ba021a929d62b1f5f85f61c935b23fdc3a07bdf01eb3d5f36f064ff4e42154", + "size": "362031" + }, + { + "host": "x86_64-pc-linux-gnu", + "url": "https://github.com/earlephilhower/mklittlefs/releases/download/2.5.1-1/x86_64-linux-gnu-mklittlefs-47e8167.tar.gz", + "archiveFileName": "x86_64-linux-gnu-mklittlefs-47e8167.tar.gz", + "checksum": "SHA-256:1e93a62cd550aa2fdb874820ead827880b964bea845a6fa053d12ddb8e39f8f0", + "size": "46226" + }, + { + "host": "x86_64-mingw32", + "url": "https://github.com/earlephilhower/mklittlefs/releases/download/2.5.1-1/x86_64-w64-mingw32-mklittlefs-47e8167.zip", + "archiveFileName": "x86_64-w64-mingw32-mklittlefs-47e8167.zip", + "checksum": "SHA-256:0136c76a710c90b0f47d9f01ab4bf46e93daba9935dd498eb9d8b52f23550626", + "size": "344669" + } + ] } ] } diff --git a/platform.txt b/platform.txt index 2ba6dfefa0..c983055be6 100644 --- a/platform.txt +++ b/platform.txt @@ -146,3 +146,7 @@ tools.esptool.upload.network_pattern="{network_cmd}" "{runtime.platform.path}/to tools.mkspiffs.cmd=mkspiffs tools.mkspiffs.cmd.windows=mkspiffs.exe tools.mkspiffs.path={runtime.platform.path}/tools/mkspiffs + +tools.mklittlefs.cmd=mklittlefs +tools.mklittlefs.cmd.windows=mklittlefs.exe +tools.mklittlefs.path={runtime.platform.path}/tools/mklittlefs diff --git a/tests/host/Makefile b/tests/host/Makefile index f4d6d9321a..1c4cc451de 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -57,6 +57,7 @@ CORE_CPP_FILES := $(addprefix $(CORE_PATH)/,\ FS.cpp \ spiffs_api.cpp \ MD5Builder.cpp \ + ../../libraries/LittleFS/src/LittleFS.cpp \ core_esp8266_noniso.cpp \ spiffs/spiffs_cache.cpp \ spiffs/spiffs_check.cpp \ @@ -81,11 +82,15 @@ CORE_CPP_FILES := $(addprefix $(CORE_PATH)/,\ $(LIBRARIES_PATH)/SD/src/SD.cpp CORE_C_FILES := $(addprefix $(CORE_PATH)/,\ + ../../libraries/LittleFS/src/lfs.c \ + ../../libraries/LittleFS/src/lfs_util.c \ ) MOCK_CPP_FILES_COMMON := $(addprefix common/,\ Arduino.cpp \ + flash_hal_mock.cpp \ spiffs_mock.cpp \ + littlefs_mock.cpp \ sdfs_mock.cpp \ WMath.cpp \ MockUART.cpp \ @@ -101,6 +106,7 @@ MOCK_CPP_FILES_EMU := $(MOCK_CPP_FILES_COMMON) $(addprefix common/,\ ArduinoMain.cpp \ ArduinoMainUdp.cpp \ ArduinoMainSpiffs.cpp \ + ArduinoMainLittlefs.cpp \ user_interface.cpp \ ) diff --git a/tests/host/common/ArduinoMain.cpp b/tests/host/common/ArduinoMain.cpp index dba9ab30fb..b0fb164ef0 100644 --- a/tests/host/common/ArduinoMain.cpp +++ b/tests/host/common/ArduinoMain.cpp @@ -44,6 +44,7 @@ bool user_exit = false; const char* host_interface = nullptr; size_t spiffs_kb = 1024; +size_t littlefs_kb = 1024; bool ignore_sigint = false; bool restore_tty = false; bool mockdebug = false; @@ -127,9 +128,10 @@ void help (const char* argv0, int exitcode) " -f - no throttle (possibly 100%%CPU)\n" " -b - blocking tty/mocked-uart (default: not blocking tty)\n" " -S - spiffs size in KBytes (default: %zd)\n" + " -L - littlefs size in KBytes (default: %zd)\n" " -v - mock verbose\n" " (negative value will force mismatched size)\n" - , argv0, MOCK_PORT_SHIFTER, spiffs_kb); + , argv0, MOCK_PORT_SHIFTER, spiffs_kb, littlefs_kb); exit(exitcode); } @@ -143,12 +145,14 @@ static struct option options[] = { "verbose", no_argument, NULL, 'v' }, { "interface", required_argument, NULL, 'i' }, { "spiffskb", required_argument, NULL, 'S' }, + { "littlefskb", required_argument, NULL, 'L' }, { "portshifter", required_argument, NULL, 's' }, }; void cleanup () { mock_stop_spiffs(); + mock_stop_littlefs(); mock_stop_uart(); } @@ -204,6 +208,9 @@ int main (int argc, char* const argv []) case 'S': spiffs_kb = atoi(optarg); break; + case 'L': + littlefs_kb = atoi(optarg); + break; case 'b': blocking_uart = true; break; @@ -226,6 +233,15 @@ int main (int argc, char* const argv []) mock_start_spiffs(name, spiffs_kb); } + if (littlefs_kb) + { + String name = argv[0]; + name += "-littlefs"; + name += String(littlefs_kb > 0? littlefs_kb: -littlefs_kb, DEC); + name += "KB"; + mock_start_littlefs(name, littlefs_kb); + } + // setup global global_ipv4_netfmt wifi_get_ip_info(0, nullptr); diff --git a/tests/host/common/flash_hal_mock.cpp b/tests/host/common/flash_hal_mock.cpp new file mode 100644 index 0000000000..5304d7553a --- /dev/null +++ b/tests/host/common/flash_hal_mock.cpp @@ -0,0 +1,36 @@ +/* Emulate the flash read/write HAL */ + +#include +#include + +extern "C" +{ + uint32_t s_phys_addr = 0; + uint32_t s_phys_size = 0; + uint32_t s_phys_page = 0; + uint32_t s_phys_block = 0; + uint8_t* s_phys_data = nullptr; +} + +int32_t flash_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { + memcpy(dst, s_phys_data + addr, size); + return 0; +} + +int32_t flash_hal_write(uint32_t addr, uint32_t size, const uint8_t *src) { + memcpy(s_phys_data + addr, src, size); + return 0; +} + +int32_t flash_hal_erase(uint32_t addr, uint32_t size) { + if ((size & (FLASH_SECTOR_SIZE - 1)) != 0 || + (addr & (FLASH_SECTOR_SIZE - 1)) != 0) { + abort(); + } + const uint32_t sector = addr / FLASH_SECTOR_SIZE; + const uint32_t sectorCount = size / FLASH_SECTOR_SIZE; + for (uint32_t i = 0; i < sectorCount; ++i) { + memset(s_phys_data + (sector + i) * FLASH_SECTOR_SIZE, 0xff, FLASH_SECTOR_SIZE); + } + return 0; +} diff --git a/tests/host/common/flash_hal_mock.h b/tests/host/common/flash_hal_mock.h new file mode 100644 index 0000000000..af5035eaa5 --- /dev/null +++ b/tests/host/common/flash_hal_mock.h @@ -0,0 +1,19 @@ +#ifndef flash_hal_mock_h +#define flash_hal_mock_h + +#include + +extern "C" +{ + extern uint32_t s_phys_addr; + extern uint32_t s_phys_size; + extern uint32_t s_phys_page; + extern uint32_t s_phys_block; + extern uint8_t* s_phys_data; +} + +extern int32_t flash_hal_read(uint32_t addr, uint32_t size, uint8_t *dst); +extern int32_t flash_hal_write(uint32_t addr, uint32_t size, const uint8_t *src); +extern int32_t flash_hal_erase(uint32_t addr, uint32_t size); + +#endif diff --git a/tests/host/common/littlefs_mock.cpp b/tests/host/common/littlefs_mock.cpp new file mode 100644 index 0000000000..0f21d7f71c --- /dev/null +++ b/tests/host/common/littlefs_mock.cpp @@ -0,0 +1,132 @@ +/* + littlefs_mock.cpp - LittleFS mock for host side testing + Copyright © 2019 Earle F. Philhower, III + + Based off spiffs_mock.cpp: + Copyright © 2016 Ivan Grokhotkov + + 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. +*/ + + +#include "littlefs_mock.h" +#include "spiffs_mock.h" +#include "spiffs/spiffs.h" +#include "debug.h" +#include +#include +#include + +#include + +#include +#include +#include +#include +#include "flash_hal_mock.h" + +#define LITTLEFS_FILE_NAME "littlefs.bin" + +FS LittleFS(nullptr); + +LittleFSMock::LittleFSMock(ssize_t fs_size, size_t fs_block, size_t fs_page, const String& storage) +{ + m_storage = storage; + if ((m_overwrite = (fs_size < 0))) + fs_size = -fs_size; + + fprintf(stderr, "LittleFS: %zd bytes\n", fs_size); + + m_fs.resize(fs_size, 0xff); + s_phys_addr = 0; + s_phys_size = static_cast(fs_size); + s_phys_page = static_cast(fs_page); + s_phys_block = static_cast(fs_block); + s_phys_data = m_fs.data(); + reset(); +} + +void LittleFSMock::reset() +{ + LittleFS = FS(FSImplPtr(new littlefs_impl::LittleFSImpl(0, s_phys_size, s_phys_page, s_phys_block, 5))); + load(); +} + +LittleFSMock::~LittleFSMock() +{ + save(); + s_phys_addr = 0; + s_phys_size = 0; + s_phys_page = 0; + s_phys_block = 0; + s_phys_data = nullptr; + m_fs.resize(0); + LittleFS = FS(FSImplPtr(nullptr)); +} + +void LittleFSMock::load () +{ + if (!m_fs.size() || !m_storage.length()) + return; + + int fs = ::open(m_storage.c_str(), O_RDONLY); + if (fs == -1) + { + fprintf(stderr, "LittleFS: loading '%s': %s\n", m_storage.c_str(), strerror(errno)); + return; + } + + off_t flen = lseek(fs, 0, SEEK_END); + if (flen == (off_t)-1) + { + fprintf(stderr, "LittleFS: checking size of '%s': %s\n", m_storage.c_str(), strerror(errno)); + return; + } + lseek(fs, 0, SEEK_SET); + + if (flen != (off_t)m_fs.size()) + { + fprintf(stderr, "LittleFS: size of '%s': %d does not match requested size %zd\n", m_storage.c_str(), (int)flen, m_fs.size()); + if (!m_overwrite) + { + fprintf(stderr, "LittleFS: aborting at user request\n"); + exit(1); + } + fprintf(stderr, "LittleFS: continuing without loading at user request, '%s' will be overwritten\n", m_storage.c_str()); + } + else + { + fprintf(stderr, "LittleFS: loading %zi bytes from '%s'\n", m_fs.size(), m_storage.c_str()); + ssize_t r = ::read(fs, m_fs.data(), m_fs.size()); + if (r != (ssize_t)m_fs.size()) + fprintf(stderr, "LittleFS: reading %zi bytes: returned %zd: %s\n", m_fs.size(), r, strerror(errno)); + } + ::close(fs); +} + +void LittleFSMock::save () +{ + if (!m_fs.size() || !m_storage.length()) + return; + + int fs = ::open(m_storage.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0644); + if (fs == -1) + { + fprintf(stderr, "LittleFS: saving '%s': %s\n", m_storage.c_str(), strerror(errno)); + return; + } + fprintf(stderr, "LittleFS: saving %zi bytes to '%s'\n", m_fs.size(), m_storage.c_str()); + + if (::write(fs, m_fs.data(), m_fs.size()) != (ssize_t)m_fs.size()) + fprintf(stderr, "LittleFS: writing %zi bytes: %s\n", m_fs.size(), strerror(errno)); + if (::close(fs) == -1) + fprintf(stderr, "LittleFS: closing %s: %s\n", m_storage.c_str(), strerror(errno)); +} diff --git a/tests/host/common/littlefs_mock.h b/tests/host/common/littlefs_mock.h new file mode 100644 index 0000000000..0905fa9322 --- /dev/null +++ b/tests/host/common/littlefs_mock.h @@ -0,0 +1,48 @@ +/* + littlefs_mock.h - LittleFS HAL mock for host side testing + Copyright © 2019 Earle F. Philhower, III + + Based on spiffs_mock: + Copyright © 2016 Ivan Grokhotkov + + 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. +*/ + +#ifndef littlefs_mock_hpp +#define littlefs_mock_hpp + +#include +#include +#include +#include +#include "flash_hal_mock.h" + +#define DEFAULT_LITTLEFS_FILE_NAME "littlefs.bin" + +class LittleFSMock { +public: + LittleFSMock(ssize_t fs_size, size_t fs_block, size_t fs_page, const String& storage = emptyString); + void reset(); + ~LittleFSMock(); + +protected: + void load (); + void save (); + + std::vector m_fs; + String m_storage; + bool m_overwrite; +}; + +#define LITTLEFS_MOCK_DECLARE(size_kb, block_kb, page_b, storage) LittleFSMock littlefs_mock(size_kb * 1024, block_kb * 1024, page_b, storage) +#define LITTLEFS_MOCK_RESET() littlefs_mock.reset() + +#endif /* littlefs_mock_hpp */ diff --git a/tests/host/common/mock.h b/tests/host/common/mock.h index e681d64975..3824b62a4b 100644 --- a/tests/host/common/mock.h +++ b/tests/host/common/mock.h @@ -135,6 +135,8 @@ void register_udp (int sock, UdpContext* udp = nullptr); void mock_start_spiffs (const String& fname, size_t size_kb, size_t block_kb = 8, size_t page_b = 512); void mock_stop_spiffs (); +void mock_start_littlefs (const String& fname, size_t size_kb, size_t block_kb = 8, size_t page_b = 512); +void mock_stop_littlefs (); // diff --git a/tests/host/common/sdfs_mock.cpp b/tests/host/common/sdfs_mock.cpp index 2a0433756b..ef2ae44152 100644 --- a/tests/host/common/sdfs_mock.cpp +++ b/tests/host/common/sdfs_mock.cpp @@ -17,5 +17,5 @@ #include "../../../libraries/SDFS/src/SDFS.h" #define SDSIZE 16LL -uint64_t _sdCardSizeB = SDSIZE * 1024LL * 1024LL; -uint8_t _sdCard[SDSIZE * 1024LL * 1024LL]; +uint64_t _sdCardSizeB = 0; +uint8_t *_sdCard = nullptr; diff --git a/tests/host/common/sdfs_mock.h b/tests/host/common/sdfs_mock.h index 5ee07f8ab9..4508e1a903 100644 --- a/tests/host/common/sdfs_mock.h +++ b/tests/host/common/sdfs_mock.h @@ -23,11 +23,21 @@ class SDFSMock { public: - SDFSMock() { } + SDFSMock(ssize_t fs_size, size_t fs_block, size_t fs_page, const String& storage = emptyString) { } void reset() { } ~SDFSMock() { } }; -#define SDFS_MOCK_DECLARE() SDFSMock sdfs_mock(); +extern uint64_t _sdCardSizeB; +extern uint8_t *_sdCard; + +#define SDFS_MOCK_DECLARE(size_kb, block_kb, page_b, storage) \ + SDFS.end(); \ + SDFSMock sdfs_mock(size_kb * 1024, block_kb * 1024, page_b, storage); free(_sdCard); \ + _sdCardSizeB = size_kb ? 16 * 1024 * 1024 : 0; \ + if (_sdCardSizeB) _sdCard = (uint8_t*)calloc(_sdCardSizeB, 1); \ + else _sdCard = nullptr; \ + SDFS.setConfig(SDFSConfig().setAutoFormat(true)); +#define SDFS_MOCK_RESET() sdfs_mock.reset() #endif /* spiffs_mock_hpp */ diff --git a/tests/host/common/spiffs_mock.cpp b/tests/host/common/spiffs_mock.cpp index 004b15fb9a..3e85fa06ab 100644 --- a/tests/host/common/spiffs_mock.cpp +++ b/tests/host/common/spiffs_mock.cpp @@ -27,14 +27,9 @@ #include #include -extern "C" -{ - static uint32_t s_phys_addr = 0; - uint32_t s_phys_size = 0; - uint32_t s_phys_page = 0; - uint32_t s_phys_block = 0; - uint8_t* s_phys_data = nullptr; -} +#include "flash_hal_mock.h" + +#define SPIFFS_FILE_NAME "spiffs.bin" FS SPIFFS(nullptr); @@ -57,7 +52,7 @@ SpiffsMock::SpiffsMock(ssize_t fs_size, size_t fs_block, size_t fs_page, const S void SpiffsMock::reset() { - SPIFFS = FS(FSImplPtr(new SPIFFSImpl(0, s_phys_size, s_phys_page, s_phys_block, 5))); + SPIFFS = FS(FSImplPtr(new spiffs_impl::SPIFFSImpl(0, s_phys_size, s_phys_page, s_phys_block, 5))); load(); } @@ -131,26 +126,3 @@ void SpiffsMock::save () if (::close(fs) == -1) fprintf(stderr, "SPIFFS: closing %s: %s\n", m_storage.c_str(), strerror(errno)); } - -int32_t spiffs_hal_read(uint32_t addr, uint32_t size, uint8_t *dst) { - memcpy(dst, s_phys_data + addr, size); - return SPIFFS_OK; -} - -int32_t spiffs_hal_write(uint32_t addr, uint32_t size, uint8_t *src) { - memcpy(s_phys_data + addr, src, size); - return SPIFFS_OK; -} - -int32_t spiffs_hal_erase(uint32_t addr, uint32_t size) { - if ((size & (FLASH_SECTOR_SIZE - 1)) != 0 || - (addr & (FLASH_SECTOR_SIZE - 1)) != 0) { - abort(); - } - const uint32_t sector = addr / FLASH_SECTOR_SIZE; - const uint32_t sectorCount = size / FLASH_SECTOR_SIZE; - for (uint32_t i = 0; i < sectorCount; ++i) { - memset(s_phys_data + (sector + i) * FLASH_SECTOR_SIZE, 0xff, FLASH_SECTOR_SIZE); - } - return SPIFFS_OK; -} diff --git a/tests/host/common/spiffs_mock.h b/tests/host/common/spiffs_mock.h index a38fd820e3..4c265964f5 100644 --- a/tests/host/common/spiffs_mock.h +++ b/tests/host/common/spiffs_mock.h @@ -20,6 +20,7 @@ #include #include #include +#include "flash_hal_mock.h" #define DEFAULT_SPIFFS_FILE_NAME "spiffs.bin" diff --git a/tests/host/fs/test_fs.cpp b/tests/host/fs/test_fs.cpp index 9cd4fcc1ea..28dd0bab5b 100644 --- a/tests/host/fs/test_fs.cpp +++ b/tests/host/fs/test_fs.cpp @@ -17,39 +17,29 @@ #include #include #include "../common/spiffs_mock.h" +#include "../common/littlefs_mock.h" #include "../common/sdfs_mock.h" #include +#include #include "../../../libraries/SDFS/src/SDFS.h" #include "../../../libraries/SD/src/SD.h" -static void createFile (const char* name, const char* content) -{ - auto f = SPIFFS.open(name, "w"); - REQUIRE(f); - if (content) { - f.print(content); - } -} - -static String readFile (const char* name) -{ - auto f = SPIFFS.open(name, "r"); - if (f) { - return f.readString(); - } - return String(); -} -static std::set listDir (const char* path) -{ - std::set result; - Dir dir = SPIFFS.openDir(path); - while (dir.next()) { - REQUIRE(result.find(dir.fileName()) == std::end(result)); - result.insert(dir.fileName()); - } - return result; -} +namespace spiffs_test { +#define FSTYPE SPIFFS +#define TESTPRE "SPIFFS - " +#define TESTPAT "[fs]" +#define TOOLONGFILENAME "/2345678901234567890123456789012" +#define FS_MOCK_DECLARE SPIFFS_MOCK_DECLARE +#define FS_MOCK_RESET SPIFFS_MOCK_RESET +#undef FS_HAS_DIRS +#include "test_fs.inc" +#undef FSTYPE +#undef TESTPRE +#undef TESTPAT +#undef TOOLONGFILENAME +#undef FS_MOCK_DECLARE +#undef FS_MOCK_RESET TEST_CASE("SPIFFS checks the config object passed in", "[fs]") { @@ -57,286 +47,90 @@ TEST_CASE("SPIFFS checks the config object passed in", "[fs]") FSConfig f; SPIFFSConfig s; SDFSConfig d; + LittleFSConfig l; REQUIRE_FALSE(SPIFFS.setConfig(f)); REQUIRE(SPIFFS.setConfig(s)); REQUIRE_FALSE(SPIFFS.setConfig(d)); + REQUIRE_FALSE(LittleFS.setConfig(l)); } +}; + + +namespace littlefs_test { +#define FSTYPE LittleFS +#define TESTPRE "LittleFS - " +#define TESTPAT "[lfs]" +// LittleFS routines strip leading slashes before doing anything, so up to 31 char names are allowable +#define TOOLONGFILENAME "/12345678901234567890123456789012" +#define FS_MOCK_DECLARE LITTLEFS_MOCK_DECLARE +#define FS_MOCK_RESET LITTLEFS_MOCK_RESET +#define FS_HAS_DIRS +#include "test_fs.inc" +#undef FSTYPE +#undef TESTPRE +#undef TESTPAT +#undef TOOLONGFILENAME +#undef FS_MOCK_DECLARE +#undef FS_MOCK_RESET + +TEST_CASE("LittleFS checks the config object passed in", "[fs]") +{ + LITTLEFS_MOCK_DECLARE(64, 8, 512, ""); + FSConfig f; + SPIFFSConfig s; + SDFSConfig d; + LittleFSConfig l; + + REQUIRE_FALSE(LittleFS.setConfig(f)); + REQUIRE_FALSE(LittleFS.setConfig(s)); + REQUIRE_FALSE(LittleFS.setConfig(d)); + REQUIRE(LittleFS.setConfig(l)); +} + +}; + +namespace sdfs_test { +#define FSTYPE SDFS +#define TESTPRE "SDFS - " +#define TESTPAT "[sdfs]" +// SDFS supports long paths (MAXPATH) +#define TOOLONGFILENAME "/" \ + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" \ + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" \ + "12345678901234567890123456789012345678901234567890123456" +#define FS_MOCK_DECLARE SDFS_MOCK_DECLARE +#define FS_MOCK_RESET SDFS_MOCK_RESET +#define FS_HAS_DIRS +#include "test_fs.inc" +#undef FSTYPE +#undef TESTPRE +#undef TESTPAT +#undef TOOLONGFILENAME +#undef FS_MOCK_DECLARE +#undef FS_MOCK_RESET + TEST_CASE("SDFS checks the config object passed in", "[fs]") { - SDFS_MOCK_DECLARE(); + SDFS_MOCK_DECLARE(64, 8, 512, ""); FSConfig f; SPIFFSConfig s; SDFSConfig d; + LittleFSConfig l; REQUIRE_FALSE(SDFS.setConfig(f)); REQUIRE_FALSE(SDFS.setConfig(s)); REQUIRE(SDFS.setConfig(d)); + REQUIRE_FALSE(SDFS.setConfig(l)); } -TEST_CASE("FS can begin","[fs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - SPIFFSConfig cfg; - cfg.setAutoFormat(false); - SPIFFS.setConfig(cfg); - REQUIRE_FALSE(SPIFFS.begin()); - cfg.setAutoFormat(true); - SPIFFS.setConfig(cfg); - REQUIRE(SPIFFS.begin()); - REQUIRE_FALSE(SPIFFS.setConfig(cfg)); // Can't change config of mounted FS -} +// Also a SD specific test to check that FILE_OPEN is really an append operation: -TEST_CASE("FS can't begin with zero size","[fs]") +TEST_CASE("SD.h FILE_WRITE macro is append", "[fs]") { - SPIFFS_MOCK_DECLARE(0, 8, 512, ""); - REQUIRE_FALSE(SPIFFS.begin()); -} - -TEST_CASE("Before begin is called, open will fail","[fs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE_FALSE(SPIFFS.open("/foo", "w")); -} - -TEST_CASE("FS can create file","[fs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.begin()); - createFile("/test", ""); - REQUIRE(SPIFFS.exists("/test")); -} - -TEST_CASE("Files can be written and appended to","[fs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.begin()); - { - File f = SPIFFS.open("config1.txt", "w"); - REQUIRE(f); - f.println("file 1"); - } - { - File f = SPIFFS.open("config1.txt", "a"); - REQUIRE(f); - f.println("file 1 again"); - } - { - File f = SPIFFS.open("config1.txt", "r"); - REQUIRE(f); - char buf[128]; - size_t len = f.read((uint8_t*)buf, sizeof(buf)); - buf[len] = 0; - REQUIRE(strcmp(buf, "file 1\r\nfile 1 again\r\n") == 0); - } -} - -TEST_CASE("Files persist after reset", "[fs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.begin()); - createFile("config1.txt", "file 1"); - - SPIFFS_MOCK_RESET(); - REQUIRE(SPIFFS.begin()); - REQUIRE(readFile("config1.txt") == "file 1"); -} - - -TEST_CASE("Filesystem is empty after format", "[fs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.format()); - REQUIRE(SPIFFS.begin()); - createFile("/1", "first"); - createFile("/2", "second"); - REQUIRE(SPIFFS.format()); - Dir root = SPIFFS.openDir("/"); - size_t count = 0; - while (root.next()) { - ++count; - } - REQUIRE(count == 0); -} - -TEST_CASE("Dir lists all files", "[fs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.begin()); - createFile("/empty", ""); - createFile("/not_empty", "some text"); - createFile("/another", "more text"); - createFile("/subdir/empty", ""); - createFile("/subdir/not_empty", "text again"); - auto files = listDir("/"); - REQUIRE(files.size() == 5); - REQUIRE(files.find("/empty") != std::end(files)); - REQUIRE(files.find("/not_empty") != std::end(files)); - REQUIRE(files.find("/another") != std::end(files)); - REQUIRE(files.find("/subdir/empty") != std::end(files)); - REQUIRE(files.find("/subdir/not_empty") != std::end(files)); -} - -TEST_CASE("File names which are too long are rejected", "[fs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.begin()); - const char* emptyName = ""; - const char* longName_31 = "/234567890123456789012345678901"; - const char* longName_32 = "/2345678901234567890123456789012"; - REQUIRE_FALSE(SPIFFS.open(emptyName, "w")); - REQUIRE_FALSE(SPIFFS.open(emptyName, "r")); - REQUIRE_FALSE(SPIFFS.exists(emptyName)); - REQUIRE_FALSE(SPIFFS.open(longName_32, "w")); - REQUIRE_FALSE(SPIFFS.open(longName_32, "r")); - REQUIRE_FALSE(SPIFFS.exists(longName_32)); - REQUIRE(SPIFFS.open(longName_31, "w")); - REQUIRE(SPIFFS.open(longName_31, "r")); - REQUIRE(SPIFFS.exists(longName_31)); -} - -TEST_CASE("#1685 Duplicate files", "[fs][bugreport]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.begin()); - createFile("/config", "some text"); - createFile("/data", ""); - readFile("/config"); - createFile("/data", "more text"); - listDir("/"); -} - -TEST_CASE("#1819 Can list all files with openDir(\"\")", "[fs][bugreport]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.begin()); - createFile("/file1", "some text"); - createFile("/file2", "other text"); - createFile("file3", "more text"); - createFile("sorta-dir/file4", "\n"); - auto files = listDir(""); - REQUIRE(files.size() == 4); -} - -TEST_CASE("truncate", "[fs][spiffs]") -{ - SPIFFS_MOCK_DECLARE(64, 8, 512, ""); - REQUIRE(SPIFFS.begin()); - createFile("/file1", "some text"); - auto f = SPIFFS.open("/file1", "r"); - f.truncate(4); - f.close(); - String s = readFile("/file1"); - REQUIRE( s == "some" ); -} - -TEST_CASE("SDFS", "[sdfs]") -{ - SDFS_MOCK_DECLARE(); - auto cfg = SDFSConfig(0, SD_SCK_MHZ(1)); - SDFS.setConfig(cfg); - REQUIRE(SDFS.format()); + SDFS_MOCK_DECLARE(64, 8, 512, ""); REQUIRE(SDFS.begin()); - REQUIRE_FALSE(SDFS.setConfig(cfg)); // Can't change config of mounted fs - REQUIRE(SDFS.mkdir("/happy/face")); - REQUIRE(SDFS.mkdir("/happy/nose")); - REQUIRE(SDFS.rmdir("/happy/face")); - auto f = SDFS.open("/this/is/a/long/name.txt", "w"); - f.printf("Hello\n"); - f.close(); - SDFS.end(); -} - -TEST_CASE("Files.ino example", "[sd]") -{ - SDFS_MOCK_DECLARE(); - SDFS.end(); - SDFS.setConfig(SDFSConfig(0, SD_SCK_MHZ(1))); - REQUIRE(SDFS.format()); - REQUIRE(SD.begin(4)); - REQUIRE_FALSE(SD.exists("example.txt")); - File myFile = SD.open("example.txt", FILE_WRITE); - REQUIRE(myFile); - myFile.close(); - REQUIRE(SD.exists("example.txt")); - REQUIRE(SD.remove("example.txt")); - REQUIRE_FALSE(SD.exists("example.txt")); - SDFS.end(); -} - - -static void createFileSD(const char* name, const char* content) -{ - auto f = SD.open(name, FILE_WRITE); - REQUIRE(f); - if (content) { - f.print(content); - } -} - -static String readFileSD(const char* name) -{ - auto f = SD.open(name, FILE_READ); - if (f) { - return f.readString(); - } - return String(); -} - -TEST_CASE("Listfiles.ino example", "[sd]") -{ - SDFS_MOCK_DECLARE(); - SDFS.setConfig(SDFSConfig(0, SD_SCK_MHZ(1))); - REQUIRE(SDFS.format()); - REQUIRE(SD.begin(4)); - - createFileSD("file1", "hello"); - createFileSD("file2", "hola"); - createFileSD("dir1/file3", "nihao"); - createFileSD("dir2/dir3/file4", "bonjour"); - - File root = SD.open("/"); - File file1 = root.openNextFile(); - File file2 = root.openNextFile(); - File dir1 = root.openNextFile(); - File dir1_file3 = dir1.openNextFile(); - File dir2 = root.openNextFile(); - File dir2_dir3 = dir2.openNextFile(); - File dir2_dir3_file4 = dir2_dir3.openNextFile(); - - bool ok; - ok = root.isDirectory() && !root.isFile() && !strcmp(root.name(), "/"); - REQUIRE(ok); - ok = !file1.isDirectory() && file1.isFile() && !strcmp(file1.name(), "file1"); - REQUIRE(ok); - ok = !file2.isDirectory() && file2.isFile() && !strcmp(file2.name(), "file2"); - REQUIRE(ok); - ok = dir1.isDirectory() && !dir1.isFile() && !strcmp(dir1.name(), "dir1"); - REQUIRE(ok); - ok = !dir1_file3.isDirectory() && dir1_file3.isFile() && !strcmp(dir1_file3.name(), "file3") && - !strcmp(dir1_file3.fullName(), "dir1/file3"); - REQUIRE(ok); - ok = dir2.isDirectory() && !dir2.isFile() && !strcmp(dir2.name(), "dir2"); - REQUIRE(ok); - ok = dir2_dir3.isDirectory() && !dir2_dir3.isFile() && !strcmp(dir2_dir3.name(), "dir3"); - REQUIRE(ok); - ok = !dir2_dir3_file4.isDirectory() && dir2_dir3_file4.isFile() && !strcmp(dir2_dir3_file4.name(), "file4") && - !strcmp(dir2_dir3_file4.fullName(), "dir2/dir3/file4"); - REQUIRE(ok); - - REQUIRE(readFileSD("/file1") == "hello"); - REQUIRE(readFileSD("file2") == "hola"); - REQUIRE(readFileSD("dir1/file3") == "nihao"); - REQUIRE(readFileSD("/dir2/dir3/file4") == "bonjour"); -} - -TEST_CASE("Multisplendored File::writes", "[fs]") -{ - SDFS_MOCK_DECLARE(); - SDFS.end(); - SDFS.setConfig(SDFSConfig(0, SD_SCK_MHZ(1))); - REQUIRE(SDFS.format()); REQUIRE(SD.begin(4)); File f = SD.open("/file.txt", FILE_WRITE); @@ -349,7 +143,11 @@ TEST_CASE("Multisplendored File::writes", "[fs]") uint32_t bigone = 0x40404040; f.write((const uint8_t*)&bigone, 4); f.close(); - REQUIRE(readFileSD("/file.txt") == "aAbbcctheendxyz@@@@"); + REQUIRE(readFile("/file.txt") == "aAbbcctheendxyz@@@@"); + f = SD.open("/file.txt", FILE_WRITE); + f.write("append", 6); + f.close(); + REQUIRE(readFile("/file.txt") == "aAbbcctheendxyz@@@@append"); File g = SD.open("/file2.txt", FILE_WRITE); g.write(0); g.close(); @@ -359,3 +157,5 @@ TEST_CASE("Multisplendored File::writes", "[fs]") g.close(); REQUIRE(u == 0); } + +}; diff --git a/tests/host/fs/test_fs.inc b/tests/host/fs/test_fs.inc new file mode 100644 index 0000000000..e6bedfa181 --- /dev/null +++ b/tests/host/fs/test_fs.inc @@ -0,0 +1,346 @@ +static void createFile (const char* name, const char* content) +{ + auto f = FSTYPE.open(name, "w"); + REQUIRE(f); + if (content) { + f.print(content); + } +} + +static String readFile (const char* name) +{ + auto f = FSTYPE.open(name, "r"); + if (f) { + return f.readString(); + } + return String(); +} + +static std::set listDir (const char* path) +{ + std::set result; + Dir dir = FSTYPE.openDir(path); + while (dir.next()) { + REQUIRE(result.find(dir.fileName()) == std::end(result)); + result.insert(dir.fileName()); + } + return result; +} + +TEST_CASE(TESTPRE "FS can begin",TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.begin()); +} + +TEST_CASE(TESTPRE "FS can't begin with zero size",TESTPAT) +{ + FS_MOCK_DECLARE(0, 8, 512, ""); + REQUIRE_FALSE(FSTYPE.begin()); +} + +TEST_CASE(TESTPRE "Before begin is called, open will fail",TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE_FALSE(FSTYPE.open("/foo", "w")); +} + +TEST_CASE(TESTPRE "FS can create file",TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + createFile("/test", ""); + REQUIRE(FSTYPE.exists("/test")); +} + +TEST_CASE(TESTPRE "Files can be written and appended to",TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + { + File f = FSTYPE.open("config1.txt", "w"); + REQUIRE(f); + f.println("file 1"); + } + { + File f = FSTYPE.open("config1.txt", "a"); + REQUIRE(f); + f.println("file 1 again"); + } + { + File f = FSTYPE.open("config1.txt", "r"); + REQUIRE(f); + char buf[128]; + size_t len = f.read((uint8_t*)buf, sizeof(buf)); + buf[len] = 0; + REQUIRE(strcmp(buf, "file 1\r\nfile 1 again\r\n") == 0); + } +} + +TEST_CASE(TESTPRE "Files persist after reset", TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + createFile("config1.txt", "file 1"); + + FS_MOCK_RESET(); + REQUIRE(FSTYPE.begin()); + REQUIRE(readFile("config1.txt") == "file 1"); +} + + +TEST_CASE(TESTPRE "Filesystem is empty after format", TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.format()); + REQUIRE(FSTYPE.begin()); + createFile("/1", "first"); + createFile("/2", "second"); + FSTYPE.end(); + REQUIRE(FSTYPE.format()); + REQUIRE(FSTYPE.begin()); + Dir root = FSTYPE.openDir("/"); + size_t count = 0; + while (root.next()) { + ++count; + } + REQUIRE(count == 0); +} + +TEST_CASE(TESTPRE "File names which are too long are rejected", TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + const char* emptyName = ""; + const char* longName_31 = "/234567890123456789012345678901"; + const char* longName_32 = TOOLONGFILENAME; + REQUIRE_FALSE(FSTYPE.open(emptyName, "w")); + REQUIRE_FALSE(FSTYPE.open(emptyName, "r")); + REQUIRE_FALSE(FSTYPE.exists(emptyName)); + REQUIRE_FALSE(FSTYPE.open(longName_32, "w")); + REQUIRE_FALSE(FSTYPE.open(longName_32, "r")); + REQUIRE_FALSE(FSTYPE.exists(longName_32)); + REQUIRE(FSTYPE.open(longName_31, "w")); + REQUIRE(FSTYPE.open(longName_31, "r")); + REQUIRE(FSTYPE.exists(longName_31)); +} + +TEST_CASE(TESTPRE "#1685 Duplicate files", "[fs][bugreport]") +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + createFile("/config", "some text"); + createFile("/data", ""); + readFile("/config"); + createFile("/data", "more text"); + auto files = listDir("/"); + REQUIRE(files.size() == 2); +} + +TEST_CASE(TESTPRE "#1819 Can list all files with openDir(\"\")", "[fs][bugreport]") +{ + FS_MOCK_DECLARE(96, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + createFile("/file1", "some text"); + createFile("/file2", "other text"); + createFile("file3", "more text"); + createFile("sorta-dir/file4", "\n"); + auto files = listDir(""); + REQUIRE(files.size() == 4); +} + +TEST_CASE(TESTPRE "truncate", TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + createFile("/file1", "some text"); + auto f = FSTYPE.open("/file1", "r+"); + REQUIRE(f.truncate(4)); + f.close(); + String s = readFile("/file1"); + REQUIRE( s == "some" ); +} + +#ifdef FS_HAS_DIRS + +#if FSTYPE != SDFS +// We silently make subdirectories if they do not exist and silently remove +// them when they're no longer needed, so make sure we can clean up after +// ourselves. At some point we may drop this and go to normal POSIX mkdir +// behavior and expose the FS::mkdir() method, but for now this works OK. +TEST_CASE(TESTPRE "Removing all files in a subdir removes that subdir", TESTPAT) +{ + FS_MOCK_DECLARE(128, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + createFile("/empty", ""); + createFile("/not_empty", "some text"); + createFile("/another", "more text"); + createFile("/subdir/empty", ""); + createFile("/subdir/not_empty", "text again"); + auto files = listDir("/"); + REQUIRE(files.size() == 4); + files = listDir("/subdir"); + REQUIRE(files.size() == 2); + // Delete one of subdir, should still exist afterwards + FSTYPE.remove("subdir/empty"); + files = listDir("/subdir"); + REQUIRE(files.size() == 1); + FSTYPE.remove("subdir/not_empty"); + files = listDir("/subdir"); + REQUIRE(files.size() == 0); + files = listDir("/"); + REQUIRE(files.size() == 3); + REQUIRE(files.find("subdir") == std::end(files)); +} +#endif + +// LittleFS openDir is slightly different than SPIFFS. In SPIFFS there +// are no directories and "/" is just another character, so "/a/b/c" is a +// file in the root dir whose name is "/a/b/c". In LittleFS we have full +// directory support, so "/a/b/c" is a file "c" in the "/a/b" dir. +// This means that if you iterate over dirOpen("/") on SPIFFS you get +// a list of every file, including "subdirs". On LittleFS, you need to +// explicitly open the subdir to see its files. This behavior is the +// same as POSIX readdir(), and helps isolate subdirs from each other. +// Also note that the returned filenames in the "dir.next()" operator +// will be in that subdir (i.e. if you opendir("/a/b"); f=dir.next();" +// f.name == "c" and not "/a/b/c" as you would see in SPIFFS. +TEST_CASE(TESTPRE "Dir lists all files", TESTPAT) +{ + FS_MOCK_DECLARE(64, 8, 512, ""); + REQUIRE(FSTYPE.begin()); + createFile("/empty", ""); + createFile("/not_empty", "some text"); + createFile("/another", "more text"); + createFile("/subdir/empty", ""); + createFile("/subdir/not_empty", "text again"); + auto files = listDir("/"); + REQUIRE(files.size() == 4); + bool empty = (files.find("/empty") != std::end(files)) || (files.find("empty") != std::end(files)); + REQUIRE(empty); + bool not_empty = (files.find("/not_empty") != std::end(files)) || (files.find("not_empty") != std::end(files)); + REQUIRE(not_empty); + bool another = (files.find("/another") != std::end(files)) || (files.find("another") != std::end(files)); + REQUIRE(another); + + files = listDir("/subdir"); + REQUIRE(files.size() == 2); + bool sub_empty = (files.find("/empty") != std::end(files)) || (files.find("empty") != std::end(files)); + REQUIRE(sub_empty); + bool sub_not_empty = (files.find("/not_empty") != std::end(files)) || (files.find("not_empty") != std::end(files)); + REQUIRE(sub_not_empty); +} + +File FindFileByName(const File f[], int count, const char *name) +{ + for (int i=0; i