diff --git a/components/fatfs/vfs/vfs_fat.c b/components/fatfs/vfs/vfs_fat.c index 6a72e80c2c..b2c0c25b06 100644 --- a/components/fatfs/vfs/vfs_fat.c +++ b/components/fatfs/vfs/vfs_fat.c @@ -39,7 +39,7 @@ typedef struct { FATFS fs; /* fatfs library FS structure */ char tmp_path_buf[FILENAME_MAX+3]; /* temporary buffer used to prepend drive name to the path */ char tmp_path_buf2[FILENAME_MAX+3]; /* as above; used in functions which take two path arguments */ - bool *o_append; /* O_APPEND is stored here for each max_files entries (because O_APPEND is not compatible with FA_OPEN_APPEND) */ + uint32_t *flags; /* file descriptor flags, array of max_files size */ #ifdef CONFIG_VFS_SUPPORT_DIR char dir_path[FILENAME_MAX]; /* variable to store path of opened directory*/ struct cached_data cached_fileinfo; @@ -85,6 +85,7 @@ static int vfs_fat_open(void* ctx, const char * path, int flags, int mode); static int vfs_fat_close(void* ctx, int fd); static int vfs_fat_fstat(void* ctx, int fd, struct stat * st); static int vfs_fat_fsync(void* ctx, int fd); +static int vfs_fat_fcntl(void* ctx, int fd, int cmd, int arg); #ifdef CONFIG_VFS_SUPPORT_DIR static int vfs_fat_stat(void* ctx, const char * path, struct stat * st); static int vfs_fat_link(void* ctx, const char* n1, const char* n2); @@ -170,6 +171,7 @@ static const esp_vfs_fs_ops_t s_vfs_fat = { .open_p = &vfs_fat_open, .close_p = &vfs_fat_close, .fstat_p = &vfs_fat_fstat, + .fcntl_p = &vfs_fat_fcntl, .fsync_p = &vfs_fat_fsync, #ifdef CONFIG_VFS_SUPPORT_DIR .dir = &s_vfs_fat_dir, @@ -199,19 +201,19 @@ esp_err_t esp_vfs_fat_register_cfg(const esp_vfs_fat_conf_t* conf, FATFS** out_f return ESP_ERR_NO_MEM; } memset(fat_ctx, 0, ctx_size); - fat_ctx->o_append = ff_memalloc(max_files * sizeof(bool)); - if (fat_ctx->o_append == NULL) { + fat_ctx->flags = ff_memalloc(max_files * sizeof(*fat_ctx->flags)); + if (fat_ctx->flags == NULL) { free(fat_ctx); return ESP_ERR_NO_MEM; } - memset(fat_ctx->o_append, 0, max_files * sizeof(bool)); + memset(fat_ctx->flags, 0, max_files * sizeof(*fat_ctx->flags)); fat_ctx->max_files = max_files; strlcpy(fat_ctx->fat_drive, conf->fat_drive, sizeof(fat_ctx->fat_drive) - 1); strlcpy(fat_ctx->base_path, conf->base_path, sizeof(fat_ctx->base_path) - 1); esp_err_t err = esp_vfs_register_fs(conf->base_path, &s_vfs_fat, ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, fat_ctx); if (err != ESP_OK) { - free(fat_ctx->o_append); + free(fat_ctx->flags); free(fat_ctx); return err; } @@ -239,7 +241,7 @@ esp_err_t esp_vfs_fat_unregister_path(const char* base_path) return err; } _lock_close(&fat_ctx->lock); - free(fat_ctx->o_append); + free(fat_ctx->flags); free(fat_ctx); s_fat_ctxs[ctx] = NULL; return ESP_OK; @@ -427,7 +429,7 @@ static int vfs_fat_open(void* ctx, const char * path, int flags, int mode) // Other VFS drivers handles O_APPEND well (to the best of my knowledge), // therefore this flag is stored here (at this VFS level) in order to save // memory. - fat_ctx->o_append[fd] = (flags & O_APPEND) == O_APPEND; + fat_ctx->flags[fd] = (flags & (O_APPEND | O_ACCMODE)); _lock_release(&fat_ctx->lock); return fd; } @@ -438,7 +440,7 @@ static ssize_t vfs_fat_write(void* ctx, int fd, const void * data, size_t size) FIL* file = &fat_ctx->files[fd]; FRESULT res; _lock_acquire(&fat_ctx->lock); - if (fat_ctx->o_append[fd]) { + if (fat_ctx->flags[fd] & O_APPEND) { if ((res = f_lseek(file, f_size(file))) != FR_OK) { ESP_LOGD(TAG, "%s: fresult=%d", __func__, res); errno = fresult_to_errno(res); @@ -672,6 +674,26 @@ static int vfs_fat_fstat(void* ctx, int fd, struct stat * st) return 0; } +static int vfs_fat_fcntl(void* ctx, int fd, int cmd, int arg) +{ + vfs_fat_ctx_t* fat_ctx = (vfs_fat_ctx_t*) ctx; + switch (cmd) { + case F_GETFL: + return fat_ctx->flags[fd]; + case F_SETFL: + fat_ctx->flags[fd] = arg; + return 0; + // no-ops: + case F_SETLK: + case F_SETLKW: + case F_GETLK: + return 0; + default: + errno = EINVAL; + return -1; + } +} + #ifdef CONFIG_VFS_SUPPORT_DIR static inline mode_t get_stat_mode(bool is_dir) diff --git a/components/newlib/realpath.c b/components/newlib/realpath.c index 6a6f1bddc8..47ad2d4ba0 100644 --- a/components/newlib/realpath.c +++ b/components/newlib/realpath.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -9,6 +9,7 @@ #include #include #include +#include #include /* realpath logic: @@ -122,3 +123,20 @@ int chdir(const char *path) errno = ENOSYS; return -1; } + +/* std::filesystem functions call chmod and exit with an exception if it fails, + * so not failing with ENOSYS seems a better solution. + */ +int chmod(const char *path, mode_t mode) +{ + return 0; +} + +/* As a workaround for libstdc++ being built with _GLIBCXX_HAVE_DIRFD, + * we have to provide at least a stub for dirfd function. + */ +int dirfd(DIR *dirp) +{ + errno = ENOSYS; + return -1; +} diff --git a/components/newlib/sysconf.c b/components/newlib/sysconf.c index 545e4a2df4..83f5a02053 100644 --- a/components/newlib/sysconf.c +++ b/components/newlib/sysconf.c @@ -1,13 +1,18 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include #include +#include +#include "esp_err.h" +#include "esp_log.h" #include "sdkconfig.h" +static const char *TAG = "sysconf"; + #ifdef CONFIG_FREERTOS_UNICORE #define CPU_NUM 1 #else @@ -25,3 +30,24 @@ long sysconf(int arg) return -1; } } + +// pathconf +long fpathconf(int fildes, int name) +{ + if (name == _PC_PATH_MAX) { + return PATH_MAX; + } + ESP_LOGW(TAG, "fpathconf: unsupported name %d", name); + errno = EINVAL; + return -1; +} + +long pathconf(const char *path, int name) +{ + if (name == _PC_PATH_MAX) { + return PATH_MAX; + } + ESP_LOGW(TAG, "pathconf: unsupported name %d", name); + errno = EINVAL; + return -1; +} diff --git a/docs/en/api-guides/cplusplus.rst b/docs/en/api-guides/cplusplus.rst index 227e146a07..6c94e0697c 100644 --- a/docs/en/api-guides/cplusplus.rst +++ b/docs/en/api-guides/cplusplus.rst @@ -13,6 +13,7 @@ The following C++ features are supported: - :ref:`cplusplus_multithreading` - :ref:`cplusplus_rtti` - :doc:`thread-local-storage` (``thread_local`` keyword) +- :ref:`cplusplus_filesystem` - All C++ features implemented by GCC, except for some :ref:`cplusplus_limitations`. See `GCC documentation `_ for details on features implemented by GCC. @@ -94,6 +95,21 @@ Enabling this option compiles all C++ files with RTTI support enabled, which all See :example:`cxx/rtti` for an example of using RTTI in ESP-IDF. Specifically, this example demonstrates how to use the RTTI feature in ESP-IDF, enabling compile time support for RTTI, and showing how to print demangled type names of objects and functions, and how dynamic_cast behaves with objects of two classes derived from a common base class. +.. _cplusplus_filesystem: + +Filesystem Library +------------------ + +C++ Filesystem library (``#include ``) features are supported in ESP-IDF, with the following exceptions: + +- Since symbolic and hard links are not supported in ESP-IDF, related functions are not implemented. +- ``std::filesystem::space`` is not implemented. +- ``std::filesystem::resize_file`` is not implemented. +- ``std::filesystem::current_path`` always returns ``/``. Setting the current path is not supported. +- ``std::filesystem::permissions`` doesn't support setting file permissions. + +Note that the choice of the filesystem also affects the behavior of the filesystem library. For example, SPIFFS filesystem has limited support for directories, therefore the related std::filesystem functions may not work as they do on a filesystem which does support directories. + Developing in C++ ----------------- @@ -186,7 +202,6 @@ Limitations - Linker script generator does not support function level placements for functions with C++ linkage. - Various section attributes (such as ``IRAM_ATTR``) are ignored when used with template functions. - Vtables are placed into Flash and are not accessible when the flash cache is disabled. Therefore, virtual function calls should be avoided in :ref:`iram-safe-interrupt-handlers`. Placement of Vtables cannot be adjusted using the linker script generator, yet. -- C++ filesystem (``std::filesystem``) features are not supported. What to Avoid diff --git a/docs/zh_CN/api-guides/cplusplus.rst b/docs/zh_CN/api-guides/cplusplus.rst index ba0c833e90..9de18fb674 100644 --- a/docs/zh_CN/api-guides/cplusplus.rst +++ b/docs/zh_CN/api-guides/cplusplus.rst @@ -13,6 +13,7 @@ ESP-IDF 支持以下 C++ 功能: - :ref:`cplusplus_multithreading` - :ref:`cplusplus_rtti` - :doc:`thread-local-storage` (``thread_local`` 关键字) +- :ref:`cplusplus_filesystem` - 除部分 :ref:`cplusplus_limitations`,所有由 GCC 实现的 C++ 功能均受支持。有关由 GCC 所实现功能的详细信息,请参阅 `GCC 文档 `_。 @@ -94,6 +95,21 @@ ESP-IDF 默认禁用对 RTTI 的支持,可以用 :ref:`CONFIG_COMPILER_CXX_RTT 有关在 ESP-IDF 中使用 RTTI 的示例,请参阅 :example:`cxx/rtti`。该示例演示了如何在 ESP-IDF 中使用 RTTI 功能,启用编译时对 RTTI 的支持,并展示了如何打印对象和函数的去混淆类型名称,以及 dynamic_cast 在两个继承自同一基类的对象上如何表现。 +.. _cplusplus_filesystem: + +文件系统库 +---------- + +ESP-IDF 支持 C++ 文件系统库 (``#include ``),但有部分功能尚未实现: + +- 由于 ESP-IDF 不支持符号链接和硬链接,因此相关函数未实现。 +- 未实现 ``std::filesystem::space``。 +- 未实现 ``std::filesystem::resize_file``。 +- ``std::filesystem::current_path`` 只返回 ``/``。不支持设置当前路径。 +- ``std::filesystem::permissions`` 不支持设置文件权限。 + +请注意,文件系统的选择也会影响文件系统库的行为。例如,SPIFFS 文件系统对目录的支持有限,因此相关的 std::filesystem 函数可能无法像在支持目录的文件系统上那样正常工作。 + 在 C++ 中进行开发 ----------------- @@ -186,7 +202,6 @@ ESP-IDF 支持 ``iostream`` 功能,但应注意: - 链接脚本生成器不支持将具有 C++ 链接的函数单独放置在内存的特定位置。 - 当与模板函数一起使用时,会忽略各种节属性(例如 ``IRAM_ATTR``)。 - vtable 位于 flash 中,在禁用 flash 缓存时无法访问。因此,在 :ref:`iram-safe-interrupt-handlers` 中应避免调用虚拟函数。目前尚无法使用链接器脚本生成器调整 vtable 的放置位置。 -- 不支持 C++ 文件系统 (``std::filesystem``) 功能。 注意事项 diff --git a/tools/test_apps/storage/.build-test-rules.yml b/tools/test_apps/storage/.build-test-rules.yml index f0ab55097c..c98bb01b98 100644 --- a/tools/test_apps/storage/.build-test-rules.yml +++ b/tools/test_apps/storage/.build-test-rules.yml @@ -38,3 +38,15 @@ tools/test_apps/storage/sdmmc_console: - sdmmc - esp_driver_sdmmc - esp_driver_sdspi + +tools/test_apps/storage/std_filesystem: + enable: + - if: IDF_TARGET in ["esp32", "esp32c3"] + reason: one Xtensa and one RISC-V chip should be enough + disable: + - if: IDF_TOOLCHAIN == "clang" + reason: Issue with C++ exceptions on Xtensa, issue with getrandom linking on RISC-V + depends_components: + - vfs + - newlib + - fatfs diff --git a/tools/test_apps/storage/std_filesystem/CMakeLists.txt b/tools/test_apps/storage/std_filesystem/CMakeLists.txt new file mode 100644 index 0000000000..077a86bb3b --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) +list(PREPEND SDKCONFIG_DEFAULTS "$ENV{IDF_PATH}/tools/test_apps/configs/sdkconfig.debug_helpers" "sdkconfig.defaults") +project(std_filesystem_test) diff --git a/tools/test_apps/storage/std_filesystem/README.md b/tools/test_apps/storage/std_filesystem/README.md new file mode 100644 index 0000000000..226ee012d7 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/README.md @@ -0,0 +1,70 @@ +| Supported Targets | ESP32 | ESP32-C3 | +| ----------------- | ----- | -------- | + +This is a test app which verifies that std::filesystem features work in ESP-IDF. The tests are written using [Catch2](https://github.com/catchorg/Catch2) managed [component](https://components.espressif.com/components/espressif/catch2/). + +To run the tests: + +```shell +idf.py flash monitor +``` + +Or, in QEMU: + +```shell +idf.py qemu monitor +``` + +Or, using pytest: + +```shell +idf.py build +pytest --embedded-services idf,qemu --target esp32 --ignore managed_components +``` + +## Feature Support + +Please update `_cplusplus_filesystem` section in cplusplus.rst when modifying this table. + +| Feature | Supported | Tested | Comment | +|------------------------------|-----------|--------|---------------------------------------------------------------------------------------------------------------| +| absolute | y | y | | +| canonical | y | y | | +| weakly_canonical | y | y | | +| relative | y | y | | +| proximate | y | y | | +| copy | y | y | this function has complex behavior, not sure about test coverage | +| copy_file | y | y | | +| copy_symlink | n | n | symlinks are not supported | +| create_directory | y | y | | +| create_directories | y | y | | +| create_hard_link | n | n | hard links are not supported | +| create_symlink | n | n | symlinks are not supported | +| create_directory_symlink | n | n | symlinks are not supported | +| current_path | partial | y | setting path is not supported in IDF | +| exists | y | y | | +| equivalent | y | y | | +| file_size | y | y | | +| hard_link_count | n | n | hard links are not supported | +| last_write_time | y | y | | +| permissions | partial | y | setting permissions is not supported | +| read_symlink | n | n | symlinks are not supported | +| remove | y | y | | +| remove_all | y | y | | +| rename | y | y | | +| resize_file | n | y | doesn't work, toolchain has to be built with _GLIBCXX_HAVE_TRUNCATE | +| space | n | y | doesn't work, toolchain has to be built with _GLIBCXX_HAVE_SYS_STATVFS_H and statvfs function must be defined | +| status | y | y | | +| symlink_status | n | n | symlinks are not supported | +| temp_directory_path | y | y | works if /tmp directory has been mounted | +| directory_iterator | y | y | | +| recursive_directory_iterator | y | y | | +| is_block_file | y | y | | +| is_character_file | y | y | | +| is_directory | y | y | | +| is_empty | y | y | | +| is_fifo | y | y | | +| is_other | n | n | | +| is_regular_file | y | y | | +| is_socket | y | y | | +| is_symlink | y | y | | diff --git a/tools/test_apps/storage/std_filesystem/main/CMakeLists.txt b/tools/test_apps/storage/std_filesystem/main/CMakeLists.txt new file mode 100644 index 0000000000..d638f7f159 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/main/CMakeLists.txt @@ -0,0 +1,10 @@ +idf_component_register(SRCS + "test_std_filesystem_main.cpp" + "test_ops.cpp" + "test_paths.cpp" + "test_status.cpp" + INCLUDE_DIRS "." + PRIV_REQUIRES vfs fatfs + WHOLE_ARCHIVE) + +fatfs_create_spiflash_image(storage ${CMAKE_CURRENT_LIST_DIR}/test_fs_image FLASH_IN_PROJECT) diff --git a/tools/test_apps/storage/std_filesystem/main/idf_component.yml b/tools/test_apps/storage/std_filesystem/main/idf_component.yml new file mode 100644 index 0000000000..15668710d3 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/main/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/catch2: "^3.7.0" diff --git a/tools/test_apps/storage/std_filesystem/main/test_fs_image/file b/tools/test_apps/storage/std_filesystem/main/test_fs_image/file new file mode 100644 index 0000000000..a32a4347a4 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/main/test_fs_image/file @@ -0,0 +1 @@ +1234567890 diff --git a/tools/test_apps/storage/std_filesystem/main/test_fs_image/test_dir_iter/dir1/f1 b/tools/test_apps/storage/std_filesystem/main/test_fs_image/test_dir_iter/dir1/f1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/test_apps/storage/std_filesystem/main/test_fs_image/test_dir_iter/dir2/dir3/f3 b/tools/test_apps/storage/std_filesystem/main/test_fs_image/test_dir_iter/dir2/dir3/f3 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/test_apps/storage/std_filesystem/main/test_ops.cpp b/tools/test_apps/storage/std_filesystem/main/test_ops.cpp new file mode 100644 index 0000000000..12d63c5265 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/main/test_ops.cpp @@ -0,0 +1,196 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include "esp_vfs_fat.h" +#include "wear_levelling.h" + +class OpsTest { +private: + wl_handle_t m_wl_handle; +public: + OpsTest() + { + esp_vfs_fat_mount_config_t mount_config = VFS_FAT_MOUNT_DEFAULT_CONFIG(); + + esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl("/test", "storage", &mount_config, &m_wl_handle); + if (err != ESP_OK) { + throw std::runtime_error("Failed to mount FAT filesystem"); + } + } + ~OpsTest() + { + esp_vfs_fat_spiflash_unmount_rw_wl("/test", m_wl_handle); + } + + void test_create_remove() + { + std::filesystem::create_directory("/test/dir"); + CHECK(std::filesystem::exists("/test/dir")); + + CHECK(std::filesystem::remove("/test/dir")); + CHECK(!std::filesystem::exists("/test/dir")); + } + + /* + The following two tests rely on the following directory structure + in the generated FAT filesystem: + + /test + └── test_dir_iter + ├── dir1 + │   └── f1 + └── dir2 + └── dir3 + └── f3 + */ + void test_directory_iterator() + { + std::filesystem::directory_iterator it("/test/test_dir_iter"); + CHECK(it != std::filesystem::directory_iterator()); + CHECK(it->path() == "/test/test_dir_iter/dir1"); + CHECK(it->is_directory()); + ++it; + CHECK(it != std::filesystem::directory_iterator()); + CHECK(it->path() == "/test/test_dir_iter/dir2"); + CHECK(it->is_directory()); + ++it; + CHECK(it == std::filesystem::directory_iterator()); + } + + void test_recursive_directory_iterator() + { + std::filesystem::recursive_directory_iterator it("/test/test_dir_iter"); + CHECK(it != std::filesystem::recursive_directory_iterator()); + CHECK(it->path() == "/test/test_dir_iter/dir1"); + CHECK(it->is_directory()); + ++it; + CHECK(it != std::filesystem::recursive_directory_iterator()); + CHECK(it->path() == "/test/test_dir_iter/dir1/f1"); + CHECK(it->is_regular_file()); + ++it; + CHECK(it != std::filesystem::recursive_directory_iterator()); + CHECK(it->path() == "/test/test_dir_iter/dir2"); + CHECK(it->is_directory()); + ++it; + CHECK(it != std::filesystem::recursive_directory_iterator()); + CHECK(it->path() == "/test/test_dir_iter/dir2/dir3"); + CHECK(it->is_directory()); + ++it; + CHECK(it != std::filesystem::recursive_directory_iterator()); + CHECK(it->path() == "/test/test_dir_iter/dir2/dir3/f3"); + CHECK(it->is_regular_file()); + ++it; + CHECK(it == std::filesystem::recursive_directory_iterator()); + } + + void test_copy_remove_recursive_copy() + { + if (std::filesystem::exists("/test/copy_dir")) { + CHECK(std::filesystem::remove_all("/test/copy_dir")); + } + + CHECK(std::filesystem::create_directory("/test/copy_dir")); + REQUIRE_NOTHROW(std::filesystem::copy("/test/test_dir_iter/dir1/f1", "/test/copy_dir/f1")); + CHECK(std::filesystem::exists("/test/copy_dir/f1")); + CHECK(std::filesystem::remove("/test/copy_dir/f1")); + CHECK(std::filesystem::remove("/test/copy_dir")); + + REQUIRE_NOTHROW(std::filesystem::copy("/test/test_dir_iter", "/test/copy_dir", std::filesystem::copy_options::recursive)); + CHECK(std::filesystem::exists("/test/copy_dir/dir1/f1")); + CHECK(std::filesystem::exists("/test/copy_dir/dir2/dir3/f3")); + CHECK(std::filesystem::remove_all("/test/copy_dir")); + } + + void test_create_directories() + { + if (std::filesystem::exists("/test/create_dir")) { + CHECK(std::filesystem::remove_all("/test/create_dir")); + } + CHECK(std::filesystem::create_directories("/test/create_dir/dir1/dir2")); + CHECK(std::filesystem::exists("/test/create_dir/dir1/dir2")); + CHECK(std::filesystem::remove_all("/test/create_dir")); + } + + void test_rename_file() + { + if (std::filesystem::exists("/test/rename_file")) { + CHECK(std::filesystem::remove("/test/rename_file")); + } + std::filesystem::create_directory("/test/rename_file"); + std::filesystem::copy_file("/test/file", "/test/rename_file/file"); + CHECK(std::filesystem::exists("/test/rename_file/file")); + std::filesystem::rename("/test/rename_file/file", "/test/rename_file/file2"); + CHECK(std::filesystem::exists("/test/rename_file/file2")); + CHECK(std::filesystem::remove_all("/test/rename_file")); + } + + void test_file_size_resize() + { + if (std::filesystem::exists("/test/file_size")) { + CHECK(std::filesystem::remove("/test/file_size")); + } + std::filesystem::copy_file("/test/file", "/test/file_size"); + CHECK(std::filesystem::file_size("/test/file_size") == 11); + // Not supported: libstdc++ has to be built with _GLIBCXX_HAVE_TRUNCATE + CHECK_THROWS(std::filesystem::resize_file("/test/file_size", 20)); + CHECK(std::filesystem::remove("/test/file_size")); + } + + void test_file_last_write_time() + { + if (std::filesystem::exists("/test/file_time")) { + CHECK(std::filesystem::remove("/test/file_time")); + } + std::filesystem::copy_file("/test/file", "/test/file_time"); + auto time = std::filesystem::last_write_time("/test/file_time"); + struct stat st = {}; + stat("/test/file_time", &st); + struct utimbuf times = {st.st_atime, st.st_mtime + 1000000000}; + utime("/test/file_time", ×); + + auto time2 = std::filesystem::last_write_time("/test/file_time"); + CHECK(time2 > time); + } + + void test_space() + { + // Not supported: libstdc++ has to be built with _GLIBCXX_HAVE_SYS_STATVFS_H and statvfs function + // has to be defined + CHECK_THROWS(std::filesystem::space("/test")); + } + + void test_permissions() + { + auto perm = std::filesystem::status("/test/file").permissions(); + CHECK(perm == std::filesystem::perms::all); + + std::filesystem::permissions("/test/file", std::filesystem::perms::owner_read, std::filesystem::perm_options::replace); + + // setting permissions is not supported and has no effect + perm = std::filesystem::status("/test/file").permissions(); + CHECK(perm == std::filesystem::perms::all); + } + + + // when adding a test method, don't forget to add it to the list below +}; + +METHOD_AS_TEST_CASE(OpsTest::test_create_remove, "Test create and remove directories"); +METHOD_AS_TEST_CASE(OpsTest::test_directory_iterator, "Test directory iterator"); +METHOD_AS_TEST_CASE(OpsTest::test_recursive_directory_iterator, "Test recursive directory iterator"); +METHOD_AS_TEST_CASE(OpsTest::test_copy_remove_recursive_copy, "Test copy, remove and recursive copy"); +METHOD_AS_TEST_CASE(OpsTest::test_create_directories, "Test create directories"); +METHOD_AS_TEST_CASE(OpsTest::test_rename_file, "Test rename file"); +METHOD_AS_TEST_CASE(OpsTest::test_file_size_resize, "Test file size and resize"); +METHOD_AS_TEST_CASE(OpsTest::test_file_last_write_time, "Test file last write time"); +METHOD_AS_TEST_CASE(OpsTest::test_space, "Test space"); +METHOD_AS_TEST_CASE(OpsTest::test_permissions, "Test permissions"); diff --git a/tools/test_apps/storage/std_filesystem/main/test_paths.cpp b/tools/test_apps/storage/std_filesystem/main/test_paths.cpp new file mode 100644 index 0000000000..92546ecb75 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/main/test_paths.cpp @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +TEST_CASE("std::filesystem path, relative, proximate, absolute") +{ + // In IDF, CWD is always in the the root directory + CHECK(std::filesystem::current_path() == "/"); + + // Create absolute path from relative path + std::filesystem::path rel_path("test/file.txt"); + std::filesystem::path abs_path = std::filesystem::absolute(rel_path); + CHECK(abs_path == "/test/file.txt"); + + // Create relative path from absolute path + std::filesystem::path rel_path2 = std::filesystem::relative(abs_path); + CHECK(rel_path2 == "test/file.txt"); + + // Create relative path from absolute path with different base + std::filesystem::path rel_path3 = std::filesystem::relative(abs_path, "/test"); + CHECK(rel_path3 == "file.txt"); + + std::filesystem::path prox_path = std::filesystem::proximate("/root1/file", "/root2"); + CHECK(prox_path == "../root1/file"); +} + +TEST_CASE("std::filesystem weakly_canonical") +{ + CHECK(std::filesystem::weakly_canonical("/a/b/c/./d/../e/f/../g") == "/a/b/c/e/g"); +} + +TEST_CASE("std::filesystem current_path") +{ + // In IDF, CWD is always in the the root directory + CHECK(std::filesystem::current_path() == "/"); + + // Changing the current path in IDF is not supported + CHECK_THROWS(std::filesystem::current_path("/test")); +} diff --git a/tools/test_apps/storage/std_filesystem/main/test_status.cpp b/tools/test_apps/storage/std_filesystem/main/test_status.cpp new file mode 100644 index 0000000000..7eeb0ce8f7 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/main/test_status.cpp @@ -0,0 +1,213 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include "esp_vfs.h" +#include "esp_err.h" +#include + +/* Helper VFS driver to test std::filesystem */ + +typedef struct { + const char* cmp_path; + int ret_errno; + struct stat ret_stat; + DIR out_dir; + int n_dir_entries; + struct dirent* ret_dirent_array; +} test_vfs_ctx_t; + +static int test_vfs_open(void* ctx, const char* path, int flags, int mode) +{ + test_vfs_ctx_t* vfs_ctx = (test_vfs_ctx_t*)ctx; + if (strcmp(path, vfs_ctx->cmp_path) != 0) { + errno = vfs_ctx->ret_errno; + return -1; + } + return 0; +} + +static int test_vfs_stat(void* ctx, const char* path, struct stat* st) +{ + test_vfs_ctx_t* vfs_ctx = (test_vfs_ctx_t*)ctx; + if (strcmp(path, vfs_ctx->cmp_path) != 0) { + errno = vfs_ctx->ret_errno; + return -1; + } + *st = vfs_ctx->ret_stat; + return 0; +} + +static DIR* test_vfs_opendir(void* ctx, const char* name) +{ + test_vfs_ctx_t* vfs_ctx = (test_vfs_ctx_t*)ctx; + if (strcmp(name, vfs_ctx->cmp_path) != 0) { + errno = vfs_ctx->ret_errno; + return nullptr; + } + return &vfs_ctx->out_dir; +} + +static struct dirent* test_vfs_readdir(void* ctx, DIR* pdir) +{ + test_vfs_ctx_t* vfs_ctx = (test_vfs_ctx_t*)ctx; + if (vfs_ctx->ret_errno) { + errno = vfs_ctx->ret_errno; + return nullptr; + } + if (vfs_ctx->n_dir_entries == 0) { + return nullptr; + } + vfs_ctx->n_dir_entries--; + struct dirent* ret = &vfs_ctx->ret_dirent_array[0]; + vfs_ctx->ret_dirent_array++; + return ret; +} + +static int test_vfs_closedir(void* ctx, DIR* pdir) +{ + return 0; +} + +/* Actual test case starts here */ + +TEST_CASE("std::filesystem status functions") +{ + test_vfs_ctx_t test_ctx = {}; + esp_vfs_t desc = {}; + desc.flags = ESP_VFS_FLAG_CONTEXT_PTR; + desc.open_p = test_vfs_open; + desc.stat_p = test_vfs_stat; + desc.opendir_p = test_vfs_opendir; + desc.readdir_p = test_vfs_readdir; + desc.closedir_p = test_vfs_closedir; + + REQUIRE(esp_vfs_register("/test", &desc, &test_ctx) == ESP_OK); + + SECTION("Test file exists") { + test_ctx.cmp_path = "/file.txt"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFREG; + + CHECK(std::filesystem::exists("/test/file.txt")); + CHECK(std::filesystem::is_regular_file("/test/file.txt")); + } + + SECTION("Test directory exists") { + test_ctx.cmp_path = "/dir"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFDIR; + + CHECK(std::filesystem::exists("/test/dir")); + CHECK(std::filesystem::is_directory("/test/dir")); + } + + SECTION("Test non-existent file") { + test_ctx.cmp_path = ""; + test_ctx.ret_errno = ENOENT; + + CHECK(!std::filesystem::exists("/test/nonexistent")); + } + + SECTION("Test is_character_file") { + test_ctx.cmp_path = "/chardev"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFCHR; + + CHECK(std::filesystem::exists("/test/chardev")); + CHECK(std::filesystem::is_character_file("/test/chardev")); + } + + SECTION("Test is_block_file") { + test_ctx.cmp_path = "/blockdev"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFBLK; + + CHECK(std::filesystem::exists("/test/blockdev")); + CHECK(std::filesystem::is_block_file("/test/blockdev")); + } + + SECTION("Test is_fifo") { + test_ctx.cmp_path = "/fifo"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFIFO; + + CHECK(std::filesystem::exists("/test/fifo")); + CHECK(std::filesystem::is_fifo("/test/fifo")); + } + + SECTION("Test is_socket") { + test_ctx.cmp_path = "/socket"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFSOCK; + + CHECK(std::filesystem::exists("/test/socket")); + CHECK(std::filesystem::is_socket("/test/socket")); + } + + SECTION("Test is_symlink") { + test_ctx.cmp_path = "/symlink"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFLNK; + + CHECK(std::filesystem::exists("/test/symlink")); + CHECK(std::filesystem::is_symlink("/test/symlink")); + } + + SECTION("Test is_empty with file") { + test_ctx.cmp_path = "/file.txt"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFREG; + test_ctx.ret_stat.st_size = 10; + + CHECK(!std::filesystem::is_empty("/test/file.txt")); + + test_ctx.ret_stat.st_size = 0; + CHECK(std::filesystem::is_empty("/test/file.txt")); + } + + SECTION("Test is_empty with directory") { + test_ctx.cmp_path = "/dir"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFDIR; + + CHECK(std::filesystem::is_empty("/test/dir")); + } + + SECTION("Test is_empty with non-empty directory") { + test_ctx.cmp_path = "/dir"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFDIR; + test_ctx.n_dir_entries = 2; + struct dirent entries[2] = { + { .d_ino = 0, .d_type = DT_REG, .d_name = "foo" }, + { .d_ino = 0, .d_type = DT_REG, .d_name = "bar" }, + }; + test_ctx.ret_dirent_array = entries; + + CHECK(!std::filesystem::is_empty("/test/dir")); + } + + SECTION("directory_iterator, empty directory") { + test_ctx.cmp_path = "/dir"; + test_ctx.ret_stat = {}; + test_ctx.ret_stat.st_mode = S_IFDIR; + test_ctx.n_dir_entries = 0; + struct dirent entries[2] = { + { .d_ino = 0, .d_type = DT_REG, .d_name = "." }, + { .d_ino = 0, .d_type = DT_REG, .d_name = ".." }, + }; + test_ctx.ret_dirent_array = entries; + + CHECK(std::filesystem::directory_iterator("/test/dir") == std::filesystem::directory_iterator{}); + } + + CHECK(esp_vfs_unregister("/test") == ESP_OK); +} diff --git a/tools/test_apps/storage/std_filesystem/main/test_std_filesystem_main.cpp b/tools/test_apps/storage/std_filesystem/main/test_std_filesystem_main.cpp new file mode 100644 index 0000000000..23a32812a9 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/main/test_std_filesystem_main.cpp @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +extern "C" void app_main(void) +{ + const char *argv[] = { + "target_test_main", + "--durations", + "yes", + NULL + }; + int argc = sizeof(argv)/sizeof(argv[0]) - 1; + + auto result = Catch::Session().run(argc, argv); + if (result != 0) { + printf("Test failed with result %d\n", result); + } else { + printf("Test passed.\n"); + } +} diff --git a/tools/test_apps/storage/std_filesystem/partitions.csv b/tools/test_apps/storage/std_filesystem/partitions.csv new file mode 100644 index 0000000000..c4bcce8510 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/partitions.csv @@ -0,0 +1,3 @@ +# Name, Type, SubType, Offset, Size, Flags +factory, app, factory, 0x10000, 1400k, +storage, data, fat, , 528k, diff --git a/tools/test_apps/storage/std_filesystem/pytest_std_filesystem.py b/tools/test_apps/storage/std_filesystem/pytest_std_filesystem.py new file mode 100644 index 0000000000..82468d1e41 --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/pytest_std_filesystem.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import pytest +from pytest_embedded import Dut + + +@pytest.mark.qemu +@pytest.mark.host_test +@pytest.mark.esp32 +@pytest.mark.esp32c3 +def test_std_filesystem(dut: Dut) -> None: + dut.expect_exact('All tests passed', timeout=200) diff --git a/tools/test_apps/storage/std_filesystem/sdkconfig.defaults b/tools/test_apps/storage/std_filesystem/sdkconfig.defaults new file mode 100644 index 0000000000..1c55b3519d --- /dev/null +++ b/tools/test_apps/storage/std_filesystem/sdkconfig.defaults @@ -0,0 +1,14 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_FATFS_LFN_HEAP=y +CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y +CONFIG_COMPILER_WARN_WRITE_STRINGS=y +CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=n +CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES=y +CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y +CONFIG_HEAP_POISONING_COMPREHENSIVE=y +CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y +CONFIG_LOG_DEFAULT_LEVEL_WARN=y +CONFIG_LOG_MAXIMUM_LEVEL_INFO=y