fs: fat: add rename
The implementation roughly follows the POSIX specification for
rename() [1]. The ordering of operations attempting to minimize the chance
for data loss in unexpected circumstances.
The 'mv' command was implemented as a front end for the rename operation
as that is what most users are likely familiar with in terms of behavior.
The 'FAT_RENAME' Kconfig option was added to prevent code size increase on
size-oriented builds like SPL.
[1] https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html
Signed-off-by: Gabriel Dalimonte <gabriel.dalimonte@gmail.com>
diff --git a/fs/fat/Kconfig b/fs/fat/Kconfig
index 9bb11ea..19d5223 100644
--- a/fs/fat/Kconfig
+++ b/fs/fat/Kconfig
@@ -13,6 +13,13 @@
This provides support for creating and writing new files to an
existing FAT filesystem partition.
+config FAT_RENAME
+ bool "Enable filesystem rename support"
+ depends on FAT_WRITE
+ help
+ This provides support for renaming and moving files within a
+ FAT filesystem partition.
+
config FS_FAT_MAX_CLUSTSIZE
int "Set maximum possible clustersize"
default 65536
diff --git a/fs/fat/fat_write.c b/fs/fat/fat_write.c
index 86366d0..d4952e2 100644
--- a/fs/fat/fat_write.c
+++ b/fs/fat/fat_write.c
@@ -1216,6 +1216,28 @@
}
/**
+ * fat_itr_parent() - modifies the iterator to the parent directory of the
+ * current iterator.
+ *
+ * @itr: iterator positioned anywhere in a directory
+ * @Return: 0 if the iterator is in the parent directory, -errno otherwise
+ */
+static int fat_itr_parent(fat_itr *itr)
+{
+ int ret;
+
+ if (itr->is_root)
+ return -EIO;
+
+ /* ensure iterator is at the first directory entry */
+ ret = fat_move_to_cluster(itr, itr->start_clust);
+ if (ret)
+ return ret;
+
+ return fat_itr_resolve(itr, "..", TYPE_DIR);
+}
+
+/**
* create_link() - inserts a directory entry for a file or directory
*
* @itr: directory iterator
@@ -1822,3 +1844,266 @@
free(dotdent);
return ret;
}
+
+/**
+ * check_path_prefix() - ensures one path does not contains another path as a
+ * prefix.
+ *
+ * for example: path foo/bar/baz/qux contains the path prefix foo/bar/baz
+ *
+ * note: the iterator may be pointing to any directory entry in the directory
+ *
+ * @prefix_clust: start cluster of the final directory in the prefix path
+ * (the start cluster of 'baz' in the above example)
+ * @path_itr: iterator of the path to check (an iterator pointing to any
+ * direntry in 'qux' in the above example)
+ * Return: -errno on error, 0 if path_itr does not have the directory
+ * at prefix_clust as an ancestor.
+ */
+static int check_path_prefix(loff_t prefix_clust, fat_itr *path_itr)
+{
+ fat_itr itr;
+ fsdata fsdata = { .fatbuf = NULL, }, *mydata = &fsdata;
+ int ret;
+
+ /* duplicate fsdata */
+ itr = *path_itr;
+ fsdata = *itr.fsdata;
+
+ /* allocate local fat buffer */
+ fsdata.fatbuf = malloc_cache_aligned(FATBUFSIZE);
+ if (!fsdata.fatbuf) {
+ log_debug("Error: allocating memory\n");
+ ret = -ENOMEM;
+ goto exit;
+ }
+
+ fsdata.fatbufnum = -1;
+ itr.fsdata = &fsdata;
+
+ /* ensure iterator is at the first directory entry */
+ ret = fat_move_to_cluster(&itr, itr.start_clust);
+ if (ret)
+ goto exit;
+
+ while (1) {
+ if (prefix_clust == itr.start_clust) {
+ ret = -EINVAL;
+ goto exit;
+ }
+
+ if (itr.is_root) {
+ ret = 0;
+ goto exit;
+ }
+
+ /* Should not occur in a well-formed FAT filesystem besides the root */
+ if (fat_itr_parent(&itr)) {
+ log_debug("FAT filesystem corrupt!\n");
+ log_debug("dir @ clust %u has no parent direntry\n",
+ itr.start_clust);
+ ret = -EIO;
+ goto exit;
+ }
+ }
+
+exit:
+ free(fsdata.fatbuf);
+ return ret;
+}
+
+/**
+ * fat_rename - rename/move a file or directory
+ *
+ * @old_path: path to the existing file/directory
+ * @new_path: new path/name for the rename/move
+ * Return: 0 on success, -errno otherwise
+ */
+int fat_rename(const char *old_path, const char *new_path)
+{
+ fat_itr *old_itr = NULL, *new_itr = NULL;
+ fsdata old_datablock = { .fatbuf = NULL, };
+ fsdata new_datablock = { .fatbuf = NULL, };
+ /* used for START macro */
+ fsdata *mydata = &old_datablock;
+ int ret = -EIO, is_old_dir;
+ char *old_path_copy, *old_dirname, *old_basename;
+ char *new_path_copy, *new_dirname, *new_basename;
+ char l_new_basename[VFAT_MAXLEN_BYTES];
+ __u32 old_clust;
+ dir_entry *found_existing;
+ /* only set if found_existing != NULL */
+ __u32 new_clust;
+
+ old_path_copy = strdup(old_path);
+ new_path_copy = strdup(new_path);
+ old_itr = malloc_cache_aligned(sizeof(fat_itr));
+ new_itr = malloc_cache_aligned(sizeof(fat_itr));
+ if (!old_path_copy || !new_path_copy || !old_itr || !new_itr) {
+ log_debug("Error: out of memory\n");
+ ret = -ENOMEM;
+ goto exit;
+ }
+ split_filename(old_path_copy, &old_dirname, &old_basename);
+ split_filename(new_path_copy, &new_dirname, &new_basename);
+
+ if (normalize_longname(l_new_basename, new_basename)) {
+ log_debug("FAT: illegal filename (%s)\n", new_basename);
+ ret = -EINVAL;
+ goto exit;
+ }
+
+ if (!strcmp(old_basename, ".") || !strcmp(old_basename, "..") ||
+ !strcmp(old_basename, "") || !strcmp(l_new_basename, ".") ||
+ !strcmp(l_new_basename, "..") || !strcmp(l_new_basename, "")) {
+ ret = -EINVAL;
+ goto exit;
+ }
+
+ /* checking for old_path == new_path is deferred until they're resolved */
+
+ /* resolve old_path */
+ ret = fat_itr_root(old_itr, &old_datablock);
+ if (ret)
+ goto exit;
+
+ ret = fat_itr_resolve(old_itr, old_dirname, TYPE_DIR);
+ if (ret) {
+ log_debug("%s doesn't exist (%d)\n", old_dirname, ret);
+ ret = -ENOENT;
+ goto exit;
+ }
+
+ if (!find_directory_entry(old_itr, old_basename)) {
+ log_debug("%s doesn't exist (%d)\n", old_basename, -ENOENT);
+ ret = -ENOENT;
+ goto exit;
+ }
+
+ /* store clust old_path points to, to relink later */
+ total_sector = old_datablock.total_sect;
+ old_clust = START(old_itr->dent);
+ is_old_dir = fat_itr_isdir(old_itr);
+
+ /* resolve new_path*/
+ ret = fat_itr_root(new_itr, &new_datablock);
+ if (ret)
+ goto exit;
+
+ ret = fat_itr_resolve(new_itr, new_dirname, TYPE_DIR);
+ if (ret) {
+ log_debug("%s doesn't exist (%d)\n", new_dirname, ret);
+ ret = -ENOENT;
+ goto exit;
+ }
+
+ found_existing = find_directory_entry(new_itr, l_new_basename);
+
+ if (found_existing) {
+ /* store cluster of new_path since it may need to be deleted */
+ new_clust = START(new_itr->dent);
+
+ /* old_path is new_path, noop */
+ if (old_clust == new_clust) {
+ ret = 0;
+ goto exit;
+ }
+
+ if (fat_itr_isdir(new_itr) != is_old_dir) {
+ if (is_old_dir)
+ ret = -ENOTDIR;
+ else
+ ret = -EISDIR;
+ goto exit;
+ }
+ }
+
+ if (is_old_dir) {
+ ret = check_path_prefix(old_clust, new_itr);
+ if (ret)
+ goto exit;
+ }
+
+ /* create/update dentry to point to old_path's data cluster */
+ if (found_existing) {
+ struct nameext new_name = new_itr->dent->nameext;
+ __u8 lcase = new_itr->dent->lcase;
+
+ if (is_old_dir) {
+ int n_entries = fat_dir_entries(new_itr);
+
+ if (n_entries < 0) {
+ ret = n_entries;
+ goto exit;
+ }
+ if (n_entries > 2) {
+ log_debug("Error: directory is not empty: %d\n",
+ n_entries);
+ ret = -ENOTEMPTY;
+ goto exit;
+ }
+ }
+
+ *new_itr->dent = *old_itr->dent;
+ new_itr->dent->nameext = new_name;
+ new_itr->dent->lcase = lcase;
+ } else {
+ /* reset iterator to the start of the directory */
+ ret = fat_move_to_cluster(new_itr, new_itr->start_clust);
+ if (ret)
+ goto exit;
+
+ ret = create_link(new_itr, l_new_basename, old_clust,
+ old_itr->dent->size,
+ old_itr->dent->attr | ATTR_ARCH);
+ if (ret)
+ goto exit;
+ }
+
+ ret = flush_dir(new_itr);
+ if (ret)
+ goto exit;
+
+ /* with new_path data cluster unreferenced, clear it */
+ if (found_existing) {
+ ret = clear_fatent(&new_datablock, new_clust);
+ if (ret)
+ goto exit;
+ }
+
+ /* update moved directory so the parent is new_path */
+ if (is_old_dir) {
+ __u32 clust = new_itr->start_clust;
+ dir_entry *dent;
+
+ fat_itr_child(new_itr, new_itr);
+ dent = find_directory_entry(new_itr, "..");
+ if (!dent) {
+ log_debug("FAT filesystem corrupt!\n");
+ log_debug("dir %s has no parent direntry\n",
+ l_new_basename);
+ ret = -EIO;
+ goto exit;
+ }
+ set_start_cluster(&new_datablock, dent, clust);
+ ret = flush_dir(new_itr);
+ if (ret)
+ goto exit;
+ }
+
+ /* refresh old in case write happened to the same block. */
+ ret = fat_move_to_cluster(old_itr, old_itr->dent_clust);
+ if (ret)
+ goto exit;
+
+ ret = delete_dentry_link(old_itr);
+exit:
+ free(new_datablock.fatbuf);
+ free(old_datablock.fatbuf);
+ free(new_itr);
+ free(old_itr);
+ free(new_path_copy);
+ free(old_path_copy);
+
+ return ret;
+}
diff --git a/fs/fs.c b/fs/fs.c
index fdff837..30a8e50 100644
--- a/fs/fs.c
+++ b/fs/fs.c
@@ -213,12 +213,16 @@
.unlink = fs_unlink_unsupported,
.mkdir = fs_mkdir_unsupported,
#endif
- .rename = fs_rename_unsupported,
.uuid = fat_uuid,
.opendir = fat_opendir,
.readdir = fat_readdir,
.closedir = fat_closedir,
.ln = fs_ln_unsupported,
+#if CONFIG_IS_ENABLED(FAT_RENAME) && !IS_ENABLED(CONFIG_XPL_BUILD)
+ .rename = fat_rename,
+#else
+ .rename = fs_rename_unsupported,
+#endif
},
#endif
@@ -1007,6 +1011,65 @@
return 0;
}
+int do_mv(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[],
+ int fstype)
+{
+ struct fs_dir_stream *dirs;
+ char *src = argv[3];
+ char *dst = argv[4];
+ char *new_dst = NULL;
+ int ret = 1;
+
+ if (argc != 5) {
+ ret = CMD_RET_USAGE;
+ goto exit;
+ }
+
+ if (fs_set_blk_dev(argv[1], argv[2], fstype))
+ goto exit;
+
+ dirs = fs_opendir(dst);
+ /* dirs being valid means dst points to an existing directory.
+ * mv should copy the file/dir (keeping the same name) into the
+ * directory
+ */
+ if (dirs) {
+ char *src_name = strrchr(src, '/');
+ int dst_len;
+
+ if (src_name)
+ src_name += 1;
+ else
+ src_name = src;
+
+ dst_len = strlen(dst);
+ new_dst = calloc(1, dst_len + strlen(src_name) + 2);
+ strcpy(new_dst, dst);
+
+ /* If there is already a trailing slash, don't add another */
+ if (new_dst[dst_len - 1] != '/') {
+ new_dst[dst_len] = '/';
+ dst_len += 1;
+ }
+
+ strcpy(new_dst + dst_len, src_name);
+ dst = new_dst;
+ }
+ fs_closedir(dirs);
+
+ if (fs_set_blk_dev(argv[1], argv[2], fstype))
+ goto exit;
+
+ if (fs_rename(src, dst))
+ goto exit;
+
+ ret = 0;
+
+exit:
+ free(new_dst);
+ return ret;
+}
+
int do_fs_types(struct cmd_tbl *cmdtp, int flag, int argc, char * const argv[])
{
struct fstype_info *drv = fstypes;