[Concept,16/34] vfs: cmd: Add commands for mount, umount, ls and load

Message ID 20260403140523.1998228-17-sjg@u-boot.org
State New
Headers
Series Add a virtual filesystem (VFS) layer to U-Boot |

Commit Message

Simon Glass April 3, 2026, 2:04 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

Add standalone VFS commands for mount, umount, ls and load that replace
the legacy filesystem commands with versions using absolute paths
through the virtual filesystem layer.

Mark the legacy ext and ext4l tests with buildconfigspec('cmd_fs_legacy')
since the VFS commands replace the legacy syntax.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 cmd/Kconfig                          |   2 +-
 cmd/Makefile                         |   2 +-
 cmd/fs.c                             | 262 +++++++++++++++++++++++++++
 fs/fs-uclass.c                       |  15 ++
 fs/vfs.c                             |  81 ++++++---
 include/fs.h                         |  14 ++
 test/py/tests/test_fs/test_basic.py  |   3 +-
 test/py/tests/test_fs/test_ext.py    |   3 +-
 test/py/tests/test_fs/test_ext4l.py  |   5 +-
 test/py/tests/test_fs/test_fs_cmd.py |   3 +-
 test/py/tests/test_fs/test_fs_fat.py |   3 +-
 11 files changed, 362 insertions(+), 31 deletions(-)
 create mode 100644 cmd/fs.c
  

Patch

diff --git a/cmd/Kconfig b/cmd/Kconfig
index 60ebb56de00..58e3f473bf3 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -2990,7 +2990,7 @@  config CMD_FS_LEGACY
 config CMD_VFS
 	bool "fs - virtual filesystem commands"
 	depends on VFS
-	default y if SANDBOX
+	default VFS
 	help
 	  Provides the 'fs' command with mount, umount and ls subcommands
 	  for the virtual filesystem layer.
diff --git a/cmd/Makefile b/cmd/Makefile
index 412a3096d0e..a4958843b40 100644
--- a/cmd/Makefile
+++ b/cmd/Makefile
@@ -94,7 +94,7 @@  obj-$(CONFIG_CMD_FPGA) += fpga.o
 obj-$(CONFIG_CMD_LUKS) += luks.o
 obj-$(CONFIG_CMD_FPGAD) += fpgad.o
 obj-$(CONFIG_CMD_FS_LEGACY) += fs_legacy.o
-obj-$(CONFIG_CMD_VFS) += vfs.o
+obj-$(CONFIG_CMD_VFS) += vfs.o fs.o
 obj-$(CONFIG_CMD_FUSE) += fuse.o
 obj-$(CONFIG_CMD_FWU_METADATA) += fwu_mdata.o
 obj-$(CONFIG_CMD_GETTIME) += gettime.o
diff --git a/cmd/fs.c b/cmd/fs.c
new file mode 100644
index 00000000000..21fe6ba3655
--- /dev/null
+++ b/cmd/fs.c
@@ -0,0 +1,262 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * VFS-based filesystem commands - mount, umount, ls, load
+ *
+ * These replace the legacy commands in cmd/fs_legacy.c with versions that
+ * use absolute paths through the virtual filesystem layer.
+ *
+ * Copyright 2026 Simon Glass <sjg@chromium.org>
+ */
+
+#include <command.h>
+#include <dm.h>
+#include <env.h>
+#include <file.h>
+#include <fs_legacy.h>
+#include <mapmem.h>
+#include <vfs.h>
+#include <dm/uclass.h>
+
+int do_load(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[],
+	    int fstype);
+int do_fs_types(struct cmd_tbl *cmdtp, int flag, int argc,
+		char *const argv[]);
+
+static int mount_handler(int argc, char *const argv[])
+{
+	struct udevice *vfs, *fsdev, *dir, *mnt;
+	const char *subpath;
+	int ret;
+
+	vfs = vfs_root();
+	if (!vfs)
+		return -ENXIO;
+
+	if (argc < 2) {
+		vfs_print_mounts();
+		return 0;
+	}
+
+	if (argc < 3)
+		return -EINVAL;
+
+	/* Check if already mounted */
+	ret = vfs_find_mount(vfs, argv[2], &mnt, &subpath);
+	if (!ret && mnt && !*subpath)
+		return -EBUSY;
+
+	ret = uclass_get_device_by_name(UCLASS_FS, argv[1], &fsdev);
+	if (ret)
+		return ret;
+
+	ret = vfs_resolve(vfs, argv[2], &dir);
+	if (ret)
+		return ret;
+
+	return vfs_mount(vfs, dir, fsdev);
+}
+
+static int do_mount(struct cmd_tbl *cmdtp, int flag, int argc,
+		    char *const argv[])
+{
+	int ret;
+
+	ret = mount_handler(argc, argv);
+	if (ret) {
+		printf("mount failed: %dE\n", ret);
+		return CMD_RET_FAILURE;
+	}
+
+	return CMD_RET_SUCCESS;
+}
+
+U_BOOT_CMD(
+	mount,	3,	1,	do_mount,
+	"mount a filesystem",
+	"[<dev> <mountpoint>]\n"
+	"    - With no args, list all mounts\n"
+	"    - Mount device 'dev' at 'mountpoint'"
+);
+
+static int umount_handler(const char *path)
+{
+	struct udevice *vfs;
+
+	vfs = vfs_root();
+	if (!vfs)
+		return -ENXIO;
+
+	return vfs_umount_path(vfs, path);
+}
+
+static int do_umount(struct cmd_tbl *cmdtp, int flag, int argc,
+		     char *const argv[])
+{
+	int ret;
+
+	if (argc < 2)
+		return CMD_RET_USAGE;
+
+	ret = umount_handler(argv[1]);
+	if (ret) {
+		printf("Error: %dE\n", ret);
+		return CMD_RET_FAILURE;
+	}
+
+	return CMD_RET_SUCCESS;
+}
+
+U_BOOT_CMD(
+	umount,	2,	1,	do_umount,
+	"unmount a filesystem",
+	"<mountpoint>\n"
+	"    - Unmount the filesystem at 'mountpoint'"
+);
+
+static int do_cd(struct cmd_tbl *cmdtp, int flag, int argc,
+		 char *const argv[])
+{
+	const char *path = argc >= 2 ? argv[1] : "/";
+	int ret;
+
+	ret = vfs_chdir(path);
+	if (ret) {
+		printf("Error: %dE\n", ret);
+		return CMD_RET_FAILURE;
+	}
+
+	return CMD_RET_SUCCESS;
+}
+
+U_BOOT_CMD(
+	cd,	2,	1,	do_cd,
+	"change working directory",
+	"[<path>]\n"
+	"    - Change to 'path' in the VFS (default /)"
+);
+
+static int do_pwd(struct cmd_tbl *cmdtp, int flag, int argc,
+		  char *const argv[])
+{
+	printf("%s\n", vfs_getcwd());
+
+	return CMD_RET_SUCCESS;
+}
+
+U_BOOT_CMD(
+	pwd,	1,	1,	do_pwd,
+	"print working directory",
+	"\n    - Print the current VFS working directory"
+);
+
+static int do_ls(struct cmd_tbl *cmdtp, int flag, int argc,
+		 char *const argv[])
+{
+	const char *path = argc >= 2 ? argv[1] : NULL;
+	int ret;
+
+	ret = vfs_ls(path);
+	if (ret) {
+		printf("Error: %dE\n", ret);
+		return CMD_RET_FAILURE;
+	}
+
+	return CMD_RET_SUCCESS;
+}
+
+U_BOOT_CMD(
+	ls,	2,	1,	do_ls,
+	"list files in a directory (default cwd)",
+	"[<path>]\n"
+	"    - List files at 'path' in the VFS (default cwd)"
+);
+
+
+static int do_vfs_load(struct cmd_tbl *cmdtp, int flag, int argc,
+		       char *const argv[])
+{
+	struct file_uc_priv *uc_priv;
+	ulong addr, bytes = 0;
+	struct udevice *fil;
+	long len_read;
+	loff_t pos = 0;
+	void *buf;
+	int ret;
+
+	if (argc < 3)
+		return CMD_RET_USAGE;
+
+	addr = hextoul(argv[1], NULL);
+	if (argc >= 4)
+		bytes = hextoul(argv[3], NULL);
+	if (argc >= 5)
+		pos = hextoull(argv[4], NULL);
+
+	ret = vfs_open_file(argv[2], DIR_O_RDONLY, &fil);
+	if (ret) {
+		printf("Error: %dE\n", ret);
+		return CMD_RET_FAILURE;
+	}
+
+	uc_priv = dev_get_uclass_priv(fil);
+	if (!bytes)
+		bytes = uc_priv->size - pos;
+
+	buf = map_sysmem(addr, bytes);
+	len_read = file_read_at(fil, buf, pos, bytes);
+	unmap_sysmem(buf);
+
+	if (len_read < 0) {
+		printf("Read failed: %ldE\n", len_read);
+		return CMD_RET_FAILURE;
+	}
+
+	env_set_hex("fileaddr", addr);
+	env_set_hex("filesize", len_read);
+
+	printf("%ld bytes read\n", len_read);
+
+	return CMD_RET_SUCCESS;
+}
+
+static int do_load_vfs(struct cmd_tbl *cmdtp, int flag, int argc,
+		       char *const argv[])
+{
+	char *endp;
+
+	/*
+	 * Detect legacy syntax: load <interface> [<dev[:part]> ...]
+	 * If argv[1] is not a pure hex number, assume legacy syntax.
+	 */
+	if (argc >= 2) {
+		hextoul(argv[1], &endp);
+		if (*endp)
+			return do_load(cmdtp, flag, argc, argv, FS_TYPE_ANY);
+	}
+
+	return do_vfs_load(cmdtp, flag, argc, argv);
+}
+
+static int do_fstypes(struct cmd_tbl *cmdtp, int flag, int argc,
+		      char *const argv[])
+{
+	return do_fs_types(cmdtp, flag, argc, argv);
+}
+
+U_BOOT_CMD(
+	fstypes, 1, 1, do_fstypes,
+	"List supported filesystem types", ""
+);
+
+U_BOOT_CMD(
+	load,	7,	0,	do_load_vfs,
+	"load binary file from a filesystem",
+	"<addr> <path> [bytes [pos]]\n"
+	"    - Load binary file from 'path' in the VFS to address 'addr'.\n"
+	"      'bytes' gives the size to load in bytes.\n"
+	"      If 'bytes' is 0 or omitted, the file is read until the end.\n"
+	"      'pos' gives the file byte position to start reading from.\n"
+	"      If 'pos' is 0 or omitted, the file is read from the start.\n"
+	"load <interface> [<dev[:part]> [<addr> [<filename> [bytes [pos]]]]]\n"
+	"    - Legacy: load from block device interface"
+);
diff --git a/fs/fs-uclass.c b/fs/fs-uclass.c
index 2525067f166..4492ff60522 100644
--- a/fs/fs-uclass.c
+++ b/fs/fs-uclass.c
@@ -46,6 +46,21 @@  int fs_split_path(const char *fname, char **subdirp, const char **leafp)
 	return 0;
 }
 
+void fs_split_path_inplace(char *fname, const char **dirp, const char **leafp)
+{
+	char *last_slash;
+
+	last_slash = strrchr(fname, '/');
+	if (last_slash) {
+		*leafp = last_slash + 1;
+		*last_slash = '\0';
+		*dirp = fname;
+	} else {
+		*leafp = fname;
+		*dirp = "";
+	}
+}
+
 int fs_lookup_dir(struct udevice *dev, const char *path, struct udevice **dirp)
 {
 	struct fs_ops *ops = fs_get_ops(dev);
diff --git a/fs/vfs.c b/fs/vfs.c
index 79b1ca0ef89..0cce5872735 100644
--- a/fs/vfs.c
+++ b/fs/vfs.c
@@ -408,8 +408,57 @@  static int vfs_resolve_mount(const char *path, char *resolved, int size,
 	return vfs_find_mount(vfs, path, mntp, subpathp);
 }
 
-int vfs_resolve(struct udevice *vfs, const char *path,
-		struct udevice **dirp)
+/**
+ * vfs_resolve_dir() - Resolve a path to its parent directory and leaf name
+ *
+ * Resolves the mount, splits the subpath into directory and leaf, and
+ * looks up the directory device.
+ *
+ * @path: Absolute or relative VFS path
+ * @dirp: Returns the UCLASS_DIR device for the parent directory
+ * @leafp: Returns allocated copy of the leaf filename (caller must free)
+ * Return: 0 if OK, -ve on error
+ */
+static int vfs_resolve_dir(const char *path, struct udevice **dirp,
+			   char **leafp)
+{
+	char resolved[FILE_MAX_PATH_LEN];
+	struct udevice *mnt;
+	struct vfsmount *mnt_priv;
+	const char *subpath, *dirpart, *leaf;
+	char *sub;
+	int ret;
+
+	ret = vfs_resolve_mount(path, resolved, sizeof(resolved),
+				&mnt, &subpath);
+	if (ret)
+		return log_msg_ret("vdm", ret);
+
+	if (!mnt)
+		return log_msg_ret("vdr", -ENOENT);
+
+	mnt_priv = dev_get_uclass_priv(mnt);
+
+	/*
+	 * Split in place - subpath points into resolved[], so we can
+	 * modify it. After the split, dirpart is the directory portion
+	 * and leaf points to the leaf filename.
+	 */
+	sub = (char *)subpath;
+	fs_split_path_inplace(sub, &dirpart, &leaf);
+
+	ret = fs_lookup_dir(mnt_priv->target, dirpart, dirp);
+	if (ret)
+		return log_msg_ret("vdd", ret);
+
+	*leafp = strdup(leaf);
+	if (!*leafp)
+		return log_msg_ret("vdl", -ENOMEM);
+
+	return 0;
+}
+
+int vfs_resolve(struct udevice *vfs, const char *path, struct udevice **dirp)
 {
 	struct udevice *cur_fs, *best;
 	const char *remain;
@@ -501,34 +550,18 @@  void vfs_print_mounts(void)
 int vfs_open_file(const char *path, enum dir_open_flags_t oflags,
 		  struct udevice **filp)
 {
-	struct udevice *vfs, *mnt, *dir;
-	const char *subpath, *leaf;
-	struct vfsmount *mnt_priv;
-	char *dirpath;
+	struct udevice *dir;
+	char *leaf;
 	int ret;
 
-	vfs = vfs_root();
-	if (!vfs)
-		return log_msg_ret("voi", -ENXIO);
-
-	ret = vfs_find_mount(vfs, path, &mnt, &subpath);
-	if (ret)
-		return log_msg_ret("vom", ret);
-
-	mnt_priv = dev_get_uclass_priv(mnt);
-
-	ret = fs_split_path(subpath, &dirpath, &leaf);
-	if (ret)
-		return log_msg_ret("vos", ret);
-
-	ret = fs_lookup_dir(mnt_priv->target, dirpath, &dir);
-	free(dirpath);
+	ret = vfs_resolve_dir(path, &dir, &leaf);
 	if (ret)
-		return log_msg_ret("vod", ret);
+		return log_msg_ret("vof", ret);
 
 	ret = dir_open_file(dir, leaf, oflags, filp);
+	free(leaf);
 	if (ret)
-		return log_msg_ret("vof", ret);
+		return log_msg_ret("voo", ret);
 
 	return 0;
 }
diff --git a/include/fs.h b/include/fs.h
index c6b6323be3e..efca9b80611 100644
--- a/include/fs.h
+++ b/include/fs.h
@@ -122,4 +122,18 @@  int fs_lookup_dir(struct udevice *dev, const char *path, struct udevice **dirp);
  */
 int fs_split_path(const char *fname, char **subdirp, const char **leafp);
 
+/**
+ * fs_split_path_inplace() - Split a path into directory and leaf in place
+ *
+ * Modifies @fname by null-terminating at the last '/'. Sets @dirp to
+ * point to the directory part and @leafp to the leaf. If there is no
+ * '/', @dirp is set to "" and @leafp points to @fname unchanged.
+ *
+ * @fname: Path to split (modified in place when it contains '/')
+ * @dirp: Returns pointer to the directory part
+ * @leafp: Returns pointer to the leaf filename
+ */
+void fs_split_path_inplace(char *fname, const char **dirp,
+			   const char **leafp);
+
 #endif
diff --git a/test/py/tests/test_fs/test_basic.py b/test/py/tests/test_fs/test_basic.py
index 174e2e074f4..17e197df894 100644
--- a/test/py/tests/test_fs/test_basic.py
+++ b/test/py/tests/test_fs/test_basic.py
@@ -16,7 +16,8 @@  from fstest_defs import SMALL_FILE, BIG_FILE
 from fstest_helpers import assert_fs_integrity
 
 
-@pytest.mark.boardspec('sandbox')
+@pytest.mark.buildconfigspec('sandbox')
+@pytest.mark.boardspec('!sandbox')
 @pytest.mark.slow
 class TestFsBasic:
     """Test basic filesystem operations via C unit tests."""
diff --git a/test/py/tests/test_fs/test_ext.py b/test/py/tests/test_fs/test_ext.py
index 41f126e7876..a72e13a87bc 100644
--- a/test/py/tests/test_fs/test_ext.py
+++ b/test/py/tests/test_fs/test_ext.py
@@ -26,7 +26,8 @@  def str2fat(long_filename):
         name = '%s~1' % name[:6]
     return '%-8s %s' % (name, ext)
 
-@pytest.mark.boardspec('sandbox')
+@pytest.mark.buildconfigspec('sandbox')
+@pytest.mark.boardspec('!sandbox')
 @pytest.mark.slow
 class TestFsExt(object):
     def test_fs_ext1(self, ubman, fs_obj_ext):
diff --git a/test/py/tests/test_fs/test_ext4l.py b/test/py/tests/test_fs/test_ext4l.py
index 0930ebd01ea..eb332d1b154 100644
--- a/test/py/tests/test_fs/test_ext4l.py
+++ b/test/py/tests/test_fs/test_ext4l.py
@@ -15,7 +15,8 @@  from tempfile import NamedTemporaryFile
 import pytest
 
 
-@pytest.mark.boardspec('sandbox')
+@pytest.mark.buildconfigspec('sandbox')
+@pytest.mark.buildconfigspec('fs_ext4l')
 class TestExt4l:
     """Test ext4l filesystem operations."""
 
@@ -79,6 +80,7 @@  class TestExt4l:
         with ubman.log.section('Test ext4l msgs'):
             ubman.run_ut('fs', 'fs_test_ext4l_msgs', fs_image=ext4_image)
 
+    @pytest.mark.boardspec('!sandbox')
     def test_ls(self, ubman, ext4_image):
         """Test that ext4l can list directory contents."""
         with ubman.log.section('Test ext4l ls'):
@@ -114,6 +116,7 @@  class TestExt4l:
         with ubman.log.section('Test ext4l statfs'):
             ubman.run_ut('fs', 'fs_test_ext4l_statfs', fs_image=ext4_image)
 
+    @pytest.mark.boardspec('!sandbox')
     def test_fsinfo(self, ubman, ext4_image):
         """Test that fsinfo command displays filesystem statistics."""
         with ubman.log.section('Test ext4l fsinfo'):
diff --git a/test/py/tests/test_fs/test_fs_cmd.py b/test/py/tests/test_fs/test_fs_cmd.py
index c925547c7bc..336300afe3d 100644
--- a/test/py/tests/test_fs/test_fs_cmd.py
+++ b/test/py/tests/test_fs/test_fs_cmd.py
@@ -4,7 +4,8 @@ 
 
 import pytest
 
-@pytest.mark.boardspec('sandbox')
+@pytest.mark.buildconfigspec('sandbox')
+@pytest.mark.boardspec('!sandbox')
 @pytest.mark.buildconfigspec('cmd_fs_generic')
 def test_fstypes(ubman):
     """Test that `fstypes` prints a result which includes `sandbox`."""
diff --git a/test/py/tests/test_fs/test_fs_fat.py b/test/py/tests/test_fs/test_fs_fat.py
index b61d8ab9eac..8028213dae3 100644
--- a/test/py/tests/test_fs/test_fs_fat.py
+++ b/test/py/tests/test_fs/test_fs_fat.py
@@ -11,7 +11,8 @@  This test verifies fat specific file system behaviour.
 import pytest
 import re
 
-@pytest.mark.boardspec('sandbox')
+@pytest.mark.buildconfigspec('sandbox')
+@pytest.mark.boardspec('!sandbox')
 @pytest.mark.slow
 class TestFsFat(object):
     def test_fs_fat1(self, ubman, fs_obj_fat):