[Concept,20/34] vfs: Add tab completion for VFS paths

Message ID 20260403140523.1998228-21-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 vfs_complete() which matches partial paths against mounted
filesystem entries, and vfs_cmd_complete() as a ready-made callback
for U_BOOT_CMD_COMPLETE.

Wire it up for the existing commands.

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

 cmd/fs.c      |  20 ++++++----
 fs/vfs.c      | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++
 include/vfs.h |  26 ++++++++++++
 test/dm/fs.c  | 104 ++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 249 insertions(+), 8 deletions(-)
  

Patch

diff --git a/cmd/fs.c b/cmd/fs.c
index 7231e4efcd1..944d15b4520 100644
--- a/cmd/fs.c
+++ b/cmd/fs.c
@@ -145,11 +145,12 @@  static int do_umount(struct cmd_tbl *cmdtp, int flag, int argc,
 	return CMD_RET_SUCCESS;
 }
 
-U_BOOT_CMD(
+U_BOOT_CMD_COMPLETE(
 	umount,	2,	1,	do_umount,
 	"unmount a filesystem",
 	"<mountpoint>\n"
-	"    - Unmount the filesystem at 'mountpoint'"
+	"    - Unmount the filesystem at 'mountpoint'",
+	vfs_cmd_complete
 );
 
 static int do_cd(struct cmd_tbl *cmdtp, int flag, int argc,
@@ -167,11 +168,12 @@  static int do_cd(struct cmd_tbl *cmdtp, int flag, int argc,
 	return CMD_RET_SUCCESS;
 }
 
-U_BOOT_CMD(
+U_BOOT_CMD_COMPLETE(
 	cd,	2,	1,	do_cd,
 	"change working directory",
 	"[<path>]\n"
-	"    - Change to 'path' in the VFS (default /)"
+	"    - Change to 'path' in the VFS (default /)",
+	vfs_cmd_complete
 );
 
 static int do_pwd(struct cmd_tbl *cmdtp, int flag, int argc,
@@ -203,11 +205,12 @@  static int do_ls(struct cmd_tbl *cmdtp, int flag, int argc,
 	return CMD_RET_SUCCESS;
 }
 
-U_BOOT_CMD(
+U_BOOT_CMD_COMPLETE(
 	ls,	2,	1,	do_ls,
 	"list files in a directory (default cwd)",
 	"[<path>]\n"
-	"    - List files at 'path' in the VFS (default cwd)"
+	"    - List files at 'path' in the VFS (default cwd)",
+	vfs_cmd_complete
 );
 
 static int do_size(struct cmd_tbl *cmdtp, int flag, int argc,
@@ -314,7 +317,7 @@  U_BOOT_CMD(
 	"List supported filesystem types", ""
 );
 
-U_BOOT_CMD(
+U_BOOT_CMD_COMPLETE(
 	load,	7,	0,	do_load_vfs,
 	"load binary file from a filesystem",
 	"<addr> <path> [bytes [pos]]\n"
@@ -324,5 +327,6 @@  U_BOOT_CMD(
 	"      '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"
+	"    - Legacy: load from block device interface",
+	vfs_cmd_complete
 );
diff --git a/fs/vfs.c b/fs/vfs.c
index 3812706af6c..328b1076280 100644
--- a/fs/vfs.c
+++ b/fs/vfs.c
@@ -12,6 +12,7 @@ 
 #define LOG_CATEGORY	UCLASS_MOUNT
 
 #include <blk.h>
+#include <ctype.h>
 #include <dir.h>
 #include <dm.h>
 #include <event.h>
@@ -751,6 +752,112 @@  int fs_mount_blkdev(const char *type, struct blk_desc *desc, int part_num,
 	return 0;
 }
 
+#ifdef CONFIG_AUTO_COMPLETE
+int vfs_complete(char *buf, const char *path, int maxv, char *cmdv[])
+{
+	char resolved[FILE_MAX_PATH_LEN];
+	struct udevice *vfs, *mnt_dev, *dir;
+	struct fs_dir_stream *strm;
+	const char *subpath, *prefix;
+	struct vfsmount *mnt;
+	struct fs_dirent dent;
+	int n = 0;
+
+	vfs = vfs_root();
+	if (!vfs)
+		return 0;
+
+	path = vfs_path_resolve(vfs_getcwd(), path, resolved, sizeof(resolved));
+	if (!path)
+		return 0;
+
+	/*
+	 * Split into directory part and prefix to match.
+	 * "/host/ar" -> dir="/host", prefix="ar"
+	 * "/host/"   -> dir="/host", prefix=""
+	 * "/"        -> dir="/", prefix=""
+	 */
+	prefix = strrchr(path, '/');
+	if (!prefix)
+		return 0;
+	prefix++;	/* skip the '/' */
+
+	/* Complete from root mount points */
+	if (prefix == path + 1) {
+		struct dir_uc_priv *uc_priv;
+
+		vfs_foreach_mount(mnt, mnt_dev) {
+			uc_priv = dev_get_uclass_priv(mnt->dir);
+			if (!strncmp(uc_priv->path, prefix, strlen(prefix))) {
+				if (n >= maxv - 1)
+					break;
+				sprintf(buf, "/%s/", uc_priv->path);
+				cmdv[n++] = buf;
+				buf += strlen(buf) + 1;
+			}
+		}
+		cmdv[n] = NULL;
+		return n;
+	}
+
+	/* Resolve the directory portion */
+	if (vfs_find_mount(vfs, path, &mnt_dev, &subpath))
+		return 0;
+
+	mnt = dev_get_uclass_priv(mnt_dev);
+
+	/* Get the directory part of subpath (everything before prefix) */
+	{
+		char dirpath[FILE_MAX_PATH_LEN];
+		int dir_len;
+
+		dir_len = prefix - 1 - (path + strlen(path) - strlen(subpath));
+		if (dir_len < 0)
+			dir_len = 0;
+		memcpy(dirpath, subpath, dir_len);
+		dirpath[dir_len] = '\0';
+
+		if (fs_lookup_dir(mnt->target, dirpath, &dir))
+			return 0;
+	}
+
+	if (dir_open(dir, &strm))
+		return 0;
+
+	while (!dir_read(dir, strm, &dent)) {
+		if (strncmp(dent.name, prefix, strlen(prefix)))
+			continue;
+		if (n >= maxv - 1)
+			break;
+		strcpy(buf, dent.name);
+		if (dent.type == FS_DT_DIR)
+			strcat(buf, "/");
+		cmdv[n++] = buf;
+		buf += strlen(buf) + 1;
+	}
+
+	dir_close(dir, strm);
+	cmdv[n] = NULL;
+
+	return n;
+}
+
+int vfs_cmd_complete(int argc, char *const argv[], char last_char,
+		     int maxv, char *cmdv[])
+{
+	static char complete_buf[2048];
+	int space = last_char == '\0' || isblank(last_char);
+
+	/* Complete the last argument as a path */
+	if (space && argc >= 1)
+		return vfs_complete(complete_buf, "", maxv, cmdv);
+	if (!space && argc >= 2)
+		return vfs_complete(complete_buf, argv[argc - 1], maxv, cmdv);
+
+	return 0;
+}
+#endif
+
 struct udevice *vfs_root(void)
 {
 	struct udevice *dev;
diff --git a/include/vfs.h b/include/vfs.h
index 6e0c67ca031..7c31ded7796 100644
--- a/include/vfs.h
+++ b/include/vfs.h
@@ -14,6 +14,7 @@ 
 #include <dir.h>
 
 struct blk_desc;
+struct cmd_tbl;
 struct disk_partition;
 struct fs_dirent;
 struct fs_statfs;
@@ -291,4 +292,29 @@  int fs_mount_blkdev(const char *type, struct blk_desc *desc, int part_num,
  */
 void vfs_print_mounts(void);
 
+#ifdef CONFIG_AUTO_COMPLETE
+/**
+ * vfs_complete() - Complete a partial VFS path
+ *
+ * Suitable for use as (or called from) a U_BOOT_CMD complete callback.
+ * Fills @cmdv with matching directory entries.
+ *
+ * @buf: Scratch buffer (must be at least FILE_MAX_PATH_LEN bytes)
+ * @path: Partial path to complete (absolute or relative)
+ * @maxv: Maximum number of entries in @cmdv
+ * @cmdv: Output array of matching names (NULL-terminated)
+ * Return: number of matches
+ */
+int vfs_complete(char *buf, const char *path, int maxv, char *cmdv[]);
+
+/**
+ * vfs_cmd_complete() - Complete callback for commands taking a VFS path
+ *
+ * Completes the last argument as a VFS path. Can be passed directly to
+ * U_BOOT_CMD_COMPLETE.
+ */
+int vfs_cmd_complete(int argc, char *const argv[], char last_char,
+		     int maxv, char *cmdv[]);
+#endif
+
 #endif
diff --git a/test/dm/fs.c b/test/dm/fs.c
index f42604fb097..0ef52ca0ee4 100644
--- a/test/dm/fs.c
+++ b/test/dm/fs.c
@@ -10,6 +10,7 @@ 
 #include <dm.h>
 #include <file.h>
 #include <fs.h>
+#include <mapmem.h>
 #include <os.h>
 #include <vfs.h>
 #include <dm/test.h>
@@ -138,6 +139,109 @@  static int dm_test_fs_file_write(struct unit_test_state *uts)
 DM_TEST(dm_test_fs_file_write, UTF_SCAN_FDT);
 
 #if IS_ENABLED(CONFIG_VFS)
+/* Helper: resolve and compare */
+static int check_resolve(struct unit_test_state *uts, const char *cwd,
+			 const char *path, const char *expected)
+{
+	char buf[256];
+	const char *ret;
+
+	ret = vfs_path_resolve(cwd, path, buf, sizeof(buf));
+	ut_assertnonnull(ret);
+	ut_asserteq_str(expected, ret);
+
+	return 0;
+}
+
+/* Test vfs_path_resolve() - no VFS device needed */
+static int dm_test_vfs_path_resolve(struct unit_test_state *uts)
+{
+	char buf[512], buf2[512];
+	int i;
+
+	/* NULL/empty path returns cwd */
+	ut_assertok(check_resolve(uts, "/", NULL, "/"));
+	ut_assertok(check_resolve(uts, "/", "", "/"));
+	ut_assertok(check_resolve(uts, "/host", NULL, "/host"));
+
+	/* Absolute path ignores cwd */
+	ut_assertok(check_resolve(uts, "/host", "/mnt", "/mnt"));
+	ut_assertok(check_resolve(uts, "/host", "/a/b", "/a/b"));
+
+	/* Relative path is joined with cwd */
+	ut_assertok(check_resolve(uts, "/", "foo", "/foo"));
+	ut_assertok(check_resolve(uts, "/host", "file.txt", "/host/file.txt"));
+	ut_assertok(check_resolve(uts, "/host", "sub/file", "/host/sub/file"));
+
+	/* . is resolved */
+	ut_assertok(check_resolve(uts, "/host", ".", "/host"));
+	ut_assertok(check_resolve(uts, "/host", "./a", "/host/a"));
+
+	/* .. is resolved */
+	ut_assertok(check_resolve(uts, "/host", "..", "/"));
+	ut_assertok(check_resolve(uts, "/host/sub", "..", "/host"));
+	ut_assertok(check_resolve(uts, "/host/a", "../b", "/host/b"));
+
+	/* .. at root stays at root */
+	ut_assertok(check_resolve(uts, "/", "..", "/"));
+	ut_assertok(check_resolve(uts, "/", "../..", "/"));
+
+	/* Absolute path with . and .. */
+	ut_assertok(check_resolve(uts, "/", "/host/./sub/..", "/host"));
+	ut_assertok(check_resolve(uts, "/", "/a/b/../c", "/a/c"));
+
+	/* Trailing slash */
+	ut_assertok(check_resolve(uts, "/", "/host/", "/host"));
+
+	/* Stack overflow returns NULL (path with >64 components) */
+	strcpy(buf, "/");
+	for (i = 0; i < 65; i++) {
+		if (i)
+			strcat(buf, "/");
+		strcat(buf, "a");
+	}
+	ut_assertnull(vfs_path_resolve("/", buf, buf2, sizeof(buf2)));
+
+	return 0;
+}
+DM_TEST(dm_test_vfs_path_resolve, 0);
+
+/* Test vfs_complete() tab completion */
+static int dm_test_vfs_complete(struct unit_test_state *uts)
+{
+	struct udevice *vfs, *fsdev, *dir;
+	char buf[2048];
+	char *cmdv[16];
+
+	ut_assertok(vfs_init());
+	vfs = vfs_root();
+	ut_assertnonnull(vfs);
+
+	ut_assertok(uclass_get_device_by_name(UCLASS_FS, "hostfs", &fsdev));
+	ut_assertok(vfs_resolve(vfs, "/host", &dir));
+	ut_assertok(vfs_mount(vfs, dir, fsdev));
+
+	/* Complete from root - should find "host" mount point */
+	ut_assert(vfs_complete(buf, "/", 16, cmdv) >= 1);
+	ut_asserteq_str("/host/", cmdv[0]);
+
+	/* Partial mount name */
+	ut_assert(vfs_complete(buf, "/h", 16, cmdv) >= 1);
+	ut_asserteq_str("/host/", cmdv[0]);
+
+	/* Non-matching prefix should return 0 */
+	ut_asserteq(0, vfs_complete(buf, "/z", 16, cmdv));
+
+	/* Complete with empty prefix shows entries */
+	ut_assert(vfs_complete(buf, "/host/", 16, cmdv) >= 1);
+
+	os_unlink(".comp_test");
+	ut_assertok(vfs_umount_path(vfs, "/host"));
+
+	return 0;
+}
+DM_TEST(dm_test_vfs_complete, UTF_SCAN_FDT);
+
 /* Test VFS init and root directory operations */
 static int dm_test_vfs_init(struct unit_test_state *uts)
 {