mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 03:00:34 +03:00
feat(fatfs): add option to make rename() replace the destination
POSIX rename() silently replaces the destination when it already exists, while FatFs' f_rename() refuses with FR_EXIST. rename() on a FAT mount therefore fails with EEXIST where the same call succeeds on other file systems, and every caller that wants portable behaviour has to remove the destination itself. Add CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION (default n, so existing behaviour is unchanged) to emulate the POSIX semantics: when f_rename() reports FR_EXIST, remove the destination and retry, all under the lock already held for the rename so no other VFS caller observes the gap. The POSIX rules on what may replace what are applied before anything is removed, because f_unlink() deletes empty directories as readily as files and would otherwise discard a directory to make way for a file: renaming a file onto a directory fails with EISDIR, a directory onto a file with ENOTDIR, and a directory onto a non-empty directory with ENOTEMPTY. Without the option all of these keep failing with EEXIST. Moving a directory into its own subtree is rejected with EINVAL as well. f_rename() does not check for this and links the directory into its own tree, losing its contents, so this is a correctness fix rather than a matter of which error is reported. Renaming an entry to itself needs no special handling: f_rename() reports FR_EXIST only when the destination resolves to a different directory entry, so a self-rename, including one written with a different spelling of the same name, already succeeds. The emulation cannot be atomic, nor can it honour the POSIX guarantee that a failed rename leaves an instance of the destination in place: FAT cannot replace a directory entry in one step, so an interruption between removing the destination and completing the rename can leave neither name. This is documented in the option's help text.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include "unity.h"
|
||||
#include "esp_partition.h"
|
||||
@@ -267,6 +268,171 @@ TEST_CASE("(WL) link copies a file, rename moves a file", "[fatfs][wear_levellin
|
||||
test_teardown();
|
||||
}
|
||||
|
||||
TEST_CASE("(WL) rename to an existing destination", "[fatfs][wear_levelling]")
|
||||
{
|
||||
test_setup();
|
||||
|
||||
const char *src = "/spiflash/ren_src.txt";
|
||||
const char *dst = "/spiflash/ren_dst.txt";
|
||||
|
||||
FILE *f = fopen(src, "w");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
TEST_ASSERT_TRUE(fputs("source", f) >= 0);
|
||||
TEST_ASSERT_EQUAL(0, fclose(f));
|
||||
|
||||
f = fopen(dst, "w");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
TEST_ASSERT_TRUE(fputs("destination", f) >= 0);
|
||||
TEST_ASSERT_EQUAL(0, fclose(f));
|
||||
|
||||
char buf[32];
|
||||
errno = 0;
|
||||
int ret = rename(src, dst);
|
||||
|
||||
#ifdef CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION
|
||||
/* POSIX behavior: the destination is replaced. */
|
||||
TEST_ASSERT_EQUAL(0, ret);
|
||||
|
||||
memset(buf, 0, sizeof(buf));
|
||||
f = fopen(dst, "r");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
TEST_ASSERT_NOT_NULL(fgets(buf, sizeof(buf), f));
|
||||
TEST_ASSERT_EQUAL(0, fclose(f));
|
||||
TEST_ASSERT_EQUAL_STRING("source", buf);
|
||||
|
||||
TEST_ASSERT_NULL(fopen(src, "r"));
|
||||
|
||||
TEST_ASSERT_EQUAL(0, unlink(dst));
|
||||
#else
|
||||
/* Default FatFs behavior: the rename is refused and nothing changes. */
|
||||
TEST_ASSERT_EQUAL(-1, ret);
|
||||
TEST_ASSERT_EQUAL(EEXIST, errno);
|
||||
|
||||
memset(buf, 0, sizeof(buf));
|
||||
f = fopen(dst, "r");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
TEST_ASSERT_NOT_NULL(fgets(buf, sizeof(buf), f));
|
||||
TEST_ASSERT_EQUAL(0, fclose(f));
|
||||
TEST_ASSERT_EQUAL_STRING("destination", buf);
|
||||
|
||||
memset(buf, 0, sizeof(buf));
|
||||
f = fopen(src, "r");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
TEST_ASSERT_NOT_NULL(fgets(buf, sizeof(buf), f));
|
||||
TEST_ASSERT_EQUAL(0, fclose(f));
|
||||
TEST_ASSERT_EQUAL_STRING("source", buf);
|
||||
|
||||
TEST_ASSERT_EQUAL(0, unlink(src));
|
||||
TEST_ASSERT_EQUAL(0, unlink(dst));
|
||||
#endif
|
||||
|
||||
test_teardown();
|
||||
}
|
||||
|
||||
TEST_CASE("(WL) rename obeys the POSIX rules on directories", "[fatfs][wear_levelling]")
|
||||
{
|
||||
test_setup();
|
||||
|
||||
const char *file = "/spiflash/ren_f.txt";
|
||||
const char *dir = "/spiflash/ren_d";
|
||||
const char *dir2 = "/spiflash/ren_d2";
|
||||
|
||||
FILE *f = fopen(file, "w");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
TEST_ASSERT_TRUE(fputs("payload", f) >= 0);
|
||||
TEST_ASSERT_EQUAL(0, fclose(f));
|
||||
TEST_ASSERT_EQUAL(0, mkdir(dir, 0755));
|
||||
|
||||
struct stat st;
|
||||
|
||||
/* A file may not replace a directory, and the directory must survive. */
|
||||
errno = 0;
|
||||
TEST_ASSERT_EQUAL(-1, rename(file, dir));
|
||||
#ifdef CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION
|
||||
TEST_ASSERT_EQUAL(EISDIR, errno);
|
||||
#else
|
||||
TEST_ASSERT_EQUAL(EEXIST, errno);
|
||||
#endif
|
||||
TEST_ASSERT_EQUAL(0, stat(dir, &st));
|
||||
TEST_ASSERT_TRUE(S_ISDIR(st.st_mode));
|
||||
|
||||
/* A directory may not replace a file, and the file must survive. */
|
||||
errno = 0;
|
||||
TEST_ASSERT_EQUAL(-1, rename(dir, file));
|
||||
#ifdef CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION
|
||||
TEST_ASSERT_EQUAL(ENOTDIR, errno);
|
||||
#else
|
||||
TEST_ASSERT_EQUAL(EEXIST, errno);
|
||||
#endif
|
||||
TEST_ASSERT_EQUAL(0, stat(file, &st));
|
||||
TEST_ASSERT_FALSE(S_ISDIR(st.st_mode));
|
||||
|
||||
/* A directory may not replace a non-empty directory. */
|
||||
TEST_ASSERT_EQUAL(0, mkdir(dir2, 0755));
|
||||
f = fopen("/spiflash/ren_d2/occupant.txt", "w");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
TEST_ASSERT_EQUAL(0, fclose(f));
|
||||
errno = 0;
|
||||
TEST_ASSERT_EQUAL(-1, rename(dir, dir2));
|
||||
#ifdef CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION
|
||||
TEST_ASSERT_EQUAL(ENOTEMPTY, errno);
|
||||
#else
|
||||
TEST_ASSERT_EQUAL(EEXIST, errno);
|
||||
#endif
|
||||
TEST_ASSERT_EQUAL(0, stat("/spiflash/ren_d2/occupant.txt", &st));
|
||||
|
||||
TEST_ASSERT_EQUAL(0, unlink("/spiflash/ren_d2/occupant.txt"));
|
||||
|
||||
/* A directory may replace an empty one. */
|
||||
errno = 0;
|
||||
int ret = rename(dir, dir2);
|
||||
#ifdef CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION
|
||||
TEST_ASSERT_EQUAL(0, ret);
|
||||
TEST_ASSERT_EQUAL(0, stat(dir2, &st));
|
||||
TEST_ASSERT_TRUE(S_ISDIR(st.st_mode));
|
||||
TEST_ASSERT_EQUAL(-1, stat(dir, &st));
|
||||
TEST_ASSERT_EQUAL(0, rmdir(dir2));
|
||||
#else
|
||||
TEST_ASSERT_EQUAL(-1, ret);
|
||||
TEST_ASSERT_EQUAL(EEXIST, errno);
|
||||
TEST_ASSERT_EQUAL(0, rmdir(dir));
|
||||
TEST_ASSERT_EQUAL(0, rmdir(dir2));
|
||||
#endif
|
||||
TEST_ASSERT_EQUAL(0, unlink(file));
|
||||
|
||||
test_teardown();
|
||||
}
|
||||
|
||||
/* Only meaningful with the option enabled: without it f_rename() happily moves
|
||||
* the directory into its own tree, which corrupts the volume, so there is no
|
||||
* safe way to exercise the case. */
|
||||
#ifdef CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION
|
||||
TEST_CASE("(WL) rename refuses to move a directory into itself", "[fatfs][wear_levelling]")
|
||||
{
|
||||
test_setup();
|
||||
|
||||
const char *dir = "/spiflash/mv_d";
|
||||
TEST_ASSERT_EQUAL(0, mkdir(dir, 0755));
|
||||
|
||||
struct stat st;
|
||||
|
||||
errno = 0;
|
||||
TEST_ASSERT_EQUAL(-1, rename(dir, "/spiflash/mv_d/child"));
|
||||
TEST_ASSERT_EQUAL(EINVAL, errno);
|
||||
TEST_ASSERT_EQUAL(0, stat(dir, &st));
|
||||
TEST_ASSERT_TRUE(S_ISDIR(st.st_mode));
|
||||
|
||||
/* A name that merely shares a prefix is a different directory, and moving
|
||||
* the directory elsewhere stays allowed. */
|
||||
TEST_ASSERT_EQUAL(0, rename(dir, "/spiflash/mv_dd"));
|
||||
TEST_ASSERT_EQUAL(0, stat("/spiflash/mv_dd", &st));
|
||||
TEST_ASSERT_TRUE(S_ISDIR(st.st_mode));
|
||||
TEST_ASSERT_EQUAL(0, rmdir("/spiflash/mv_dd"));
|
||||
|
||||
test_teardown();
|
||||
}
|
||||
#endif // CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION
|
||||
|
||||
TEST_CASE("(WL) can create and remove directories", "[fatfs][wear_levelling]")
|
||||
{
|
||||
test_setup();
|
||||
|
||||
@@ -15,6 +15,7 @@ from pytest_embedded_idf.utils import idf_parametrize
|
||||
'fastseek',
|
||||
'auto_fsync',
|
||||
'dyn_buffers',
|
||||
'posix_rename',
|
||||
],
|
||||
)
|
||||
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION=y
|
||||
Reference in New Issue
Block a user