[Concept,15/34] vfs: Add current working directory and path helpers

Message ID 20260403140523.1998228-16-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 a current working directory (cwd) to the VFS layer, stored in the
vfs_rootfs driver-private data, with vfs_chdir() and vfs_getcwd() to
change and query it.

Add vfs_resolve_cwd() which resolves relative paths against the cwd,
and use it in vfs_ls() so that relative paths work.

Add vfs_resolve_mount() which combines vfs_root lookup, cwd resolution
and mount lookup in one call.

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

 fs/vfs.c          | 194 ++++++++++++++++++++++++++++++++++++++++++----
 fs/vfs_internal.h |   4 +
 include/vfs.h     |  30 +++++++
 test/dm/fs.c      |  42 ++++++++++
 4 files changed, 256 insertions(+), 14 deletions(-)
  

Patch

diff --git a/fs/vfs.c b/fs/vfs.c
index f06844f2a33..79b1ca0ef89 100644
--- a/fs/vfs.c
+++ b/fs/vfs.c
@@ -11,6 +11,7 @@ 
 
 #define LOG_CATEGORY	UCLASS_MOUNT
 
+#include <blk.h>
 #include <dir.h>
 #include <dm.h>
 #include <event.h>
@@ -30,6 +31,139 @@ 
 	     (pos) && ((mnt) = dev_get_uclass_priv(pos)); \
 	     uclass_next_device(&(pos)))
 
+/**
+ * canonicalise_path() - Remove . and .. components from an absolute path
+ *
+ * Modifies @buf in place. The path must start with '/'.
+ *
+ * @buf: Absolute path to canonicalise (modified in place)
+ */
+static int canonicalise_path(char *buf)
+{
+	char *p, *token;
+	char *stack[64];
+	int depth = 0;
+
+	/* Tokenise by '/' and resolve . and .. */
+	p = buf + 1;	/* skip leading '/' */
+	while (*p) {
+		token = p;
+		while (*p && *p != '/')
+			p++;
+		if (*p)
+			*p++ = '\0';
+
+		if (!*token || !strcmp(token, ".")) {
+			continue;
+		} else if (!strcmp(token, "..")) {
+			if (depth > 0)
+				depth--;
+		} else {
+			if (depth >= ARRAY_SIZE(stack))
+				return -ENAMETOOLONG;
+			stack[depth++] = token;
+		}
+	}
+
+	/*
+	 * Rebuild the path from the stack. This always fits in buf since
+	 * removing . and .. components can only shorten the path.
+	 */
+	p = buf;
+	*p++ = '/';
+	for (int i = 0; i < depth; i++) {
+		if (i > 0)
+			*p++ = '/';
+		strcpy(p, stack[i]);
+		p += strlen(stack[i]);
+	}
+	*p = '\0';
+
+	/* Ensure root is "/" not "" */
+	if (!buf[1])
+		buf[0] = '/';
+
+	return 0;
+}
+
+const char *vfs_path_resolve(const char *cwd, const char *path, char *buf,
+			     int size)
+{
+	if (!path || !*path) {
+		strncpy(buf, cwd, size);
+		buf[size - 1] = '\0';
+		return buf;
+	}
+
+	if (*path == '/') {
+		strncpy(buf, path, size);
+		buf[size - 1] = '\0';
+	} else {
+		int len = strlen(cwd);
+
+		if (len == 1)
+			snprintf(buf, size, "/%s", path);
+		else
+			snprintf(buf, size, "%s/%s", cwd, path);
+	}
+
+	if (canonicalise_path(buf))
+		return NULL;
+
+	return buf;
+}
+
+const char *vfs_getcwd(void)
+{
+	struct udevice *vfs = vfs_root();
+	struct vfs_priv *priv;
+
+	if (!vfs)
+		return "/";
+	priv = dev_get_priv(vfs);
+
+	return priv->cwd;
+}
+
+int vfs_chdir(const char *path)
+{
+	char resolved[FILE_MAX_PATH_LEN];
+	struct udevice *vfs, *mnt;
+	const char *subpath, *abs;
+	struct vfs_priv *priv;
+	int len, ret;
+
+	vfs = vfs_root();
+	if (!vfs)
+		return log_msg_ret("vci", -ENXIO);
+	priv = dev_get_priv(vfs);
+
+	abs = vfs_path_resolve(vfs_getcwd(), path, resolved, sizeof(resolved));
+	if (!abs)
+		return log_msg_ret("vcp", -ENAMETOOLONG);
+
+	/* Verify the path exists by trying to resolve it */
+	if (strcmp(abs, "/")) {
+		ret = vfs_find_mount(vfs, abs, &mnt, &subpath);
+		if (ret)
+			return log_msg_ret("vcf", ret);
+	}
+
+	len = strlen(abs);
+
+	/* Strip trailing slash (but keep "/" for root) */
+	if (len > 1 && abs[len - 1] == '/')
+		len--;
+
+	if (len >= sizeof(priv->cwd))
+		return log_msg_ret("vcl", -ENAMETOOLONG);
+
+	memcpy(priv->cwd, abs, len);
+	priv->cwd[len] = '\0';
+
+	return 0;
+}
+
 /**
  * find_mount() - Check whether a directory is a mount point
  *
@@ -174,10 +308,13 @@  static int vfs_mount_path(struct udevice *mnt_dev, char *buf, int size)
 static int vfs_rootfs_mount(struct udevice *dev)
 {
 	struct fs_priv *uc_priv = dev_get_uclass_priv(dev);
+	struct vfs_priv *priv = dev_get_priv(dev);
 
 	if (uc_priv->mounted)
 		return log_msg_ret("rfm", -EISCONN);
 
+	strcpy(priv->cwd, "/");
+
 	uc_priv->mounted = true;
 
 	return 0;
@@ -243,6 +380,34 @@  int vfs_find_mount(struct udevice *vfs, const char *path, struct udevice **mntp,
 	return 0;
 }
 
+/**
+ * vfs_resolve_mount() - Resolve a path to its mount and subpath
+ *
+ * Handles vfs_root lookup, cwd resolution and mount lookup in one call.
+ *
+ * @path: Absolute or relative VFS path
+ * @resolved: Buffer for cwd-resolved path
+ * @size: Size of @resolved
+ * @mntp: Returns the UCLASS_MOUNT device (NULL for root)
+ * @subpathp: Returns the remaining path within the mount
+ * Return: 0 if OK, -ve on error
+ */
+static int vfs_resolve_mount(const char *path, char *resolved, int size,
+			     struct udevice **mntp, const char **subpathp)
+{
+	struct udevice *vfs;
+
+	vfs = vfs_root();
+	if (!vfs)
+		return log_msg_ret("vrv", -ENXIO);
+
+	path = vfs_path_resolve(vfs_getcwd(), path, resolved, size);
+	if (!path)
+		return log_msg_ret("vrp", -ENAMETOOLONG);
+
+	return vfs_find_mount(vfs, path, mntp, subpathp);
+}
+
 int vfs_resolve(struct udevice *vfs, const char *path,
 		struct udevice **dirp)
 {
@@ -296,11 +461,13 @@  int vfs_umount(struct udevice *mnt_dev)
 
 int vfs_umount_path(struct udevice *vfs, const char *path)
 {
+	char resolved[FILE_MAX_PATH_LEN];
 	struct udevice *mnt_dev;
 	const char *subpath;
 	int ret;
 
-	ret = vfs_find_mount(vfs, path, &mnt_dev, &subpath);
+	ret = vfs_resolve_mount(path, resolved, sizeof(resolved),
+				&mnt_dev, &subpath);
 	if (ret)
 		return log_msg_ret("vuf", ret);
 
@@ -368,32 +535,31 @@  int vfs_open_file(const char *path, enum dir_open_flags_t oflags,
 
 int vfs_ls(const char *path)
 {
-	struct udevice *vfs, *mnt, *dir = NULL;
+	char resolved[FILE_MAX_PATH_LEN];
+	struct udevice *mnt, *dir = NULL;
 	struct fs_dir_stream *strm;
 	struct fs_dirent dent;
 	const char *subpath;
 	bool empty = true;
 	int ret;
 
-	vfs = vfs_root();
-	if (!vfs)
-		return -ENXIO;
+	ret = vfs_resolve_mount(path, resolved, sizeof(resolved),
+				&mnt, &subpath);
+	if (ret)
+		return ret;
 
-	ret = vfs_find_mount(vfs, path, &mnt, &subpath);
-	if (!ret && mnt) {
+	if (mnt) {
 		struct vfsmount *m = dev_get_uclass_priv(mnt);
 
 		ret = fs_lookup_dir(m->target, subpath, &dir);
-		if (ret)
-			return ret;
-	} else if (!ret || !path[1]) {
+	} else {
 		/* Root "/" - list the VFS root dir */
+		struct udevice *vfs = vfs_root();
+
 		ret = fs_lookup_dir(vfs, "", &dir);
-		if (ret)
-			return ret;
-	} else {
-		return ret;
 	}
+	if (ret)
+		return ret;
 
 	ret = dir_open(dir, &strm);
 	if (ret)
diff --git a/fs/vfs_internal.h b/fs/vfs_internal.h
index ac7f756e24f..36539741402 100644
--- a/fs/vfs_internal.h
+++ b/fs/vfs_internal.h
@@ -8,15 +8,19 @@ 
 #ifndef __VFS_INTERNAL_H
 #define __VFS_INTERNAL_H
 
+#include <file.h>
+
 struct udevice;
 
 /**
  * struct vfs_priv - VFS root FS device driver-private data
  *
  * @mount_count: Counter for unique mount device names
+ * @cwd: Current working directory (absolute path)
  */
 struct vfs_priv {
 	int mount_count;
+	char cwd[FILE_MAX_PATH_LEN];
 };
 
 /**
diff --git a/include/vfs.h b/include/vfs.h
index 53d1932e861..521494e4199 100644
--- a/include/vfs.h
+++ b/include/vfs.h
@@ -41,6 +41,36 @@  struct vfsmount {
  */
 int vfs_init(void);
 
+/**
+ * vfs_chdir() - Change the current working directory
+ *
+ * @path: New directory (absolute or relative to current cwd)
+ * Return: 0 if OK, -ve on error
+ */
+int vfs_chdir(const char *path);
+
+/**
+ * vfs_getcwd() - Get the current working directory
+ *
+ * Return: pointer to the cwd string (static buffer, do not free)
+ */
+const char *vfs_getcwd(void);
+
+/**
+ * vfs_path_resolve() - Resolve a path against a given working directory
+ *
+ * If @path starts with '/', it is used as an absolute path. Otherwise it
+ * is joined with @cwd. In both cases, '.' and '..' components are resolved.
+ *
+ * @cwd: Current working directory (must be absolute)
+ * @path: Input path (absolute or relative), or NULL/empty for cwd
+ * @buf: Buffer to write the resolved absolute path
+ * @size: Size of @buf
+ * Return: pointer to @buf
+ */
+const char *vfs_path_resolve(const char *cwd, const char *path,
+			     char *buf, int size);
+
 /**
  * vfs_root() - Get the VFS root FS device
  *
diff --git a/test/dm/fs.c b/test/dm/fs.c
index b14083590c6..e45441e5241 100644
--- a/test/dm/fs.c
+++ b/test/dm/fs.c
@@ -284,4 +284,46 @@  static int dm_test_vfs_cmd(struct unit_test_state *uts)
 }
 DM_TEST(dm_test_vfs_cmd, UTF_SCAN_FDT);
 
+/* Test current working directory */
+static int dm_test_vfs_cwd(struct unit_test_state *uts)
+{
+	struct udevice *vfs, *fsdev, *dir;
+
+	ut_assertok(vfs_init());
+	vfs = vfs_root();
+	ut_assertnonnull(vfs);
+
+	/* Default cwd is "/" */
+	ut_asserteq_str("/", vfs_getcwd());
+
+	/* Mount sandbox FS at /host */
+	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));
+
+	/* cd to /host */
+	ut_assertok(vfs_chdir("/host"));
+	ut_asserteq_str("/host", vfs_getcwd());
+
+	/* ls with NULL should list cwd (i.e. /host) */
+	console_record_reset_enable();
+	ut_assertok(vfs_ls(NULL));
+	ut_assert_skip_to_linen("DIR ");
+	console_record_reset_enable();
+
+	/* cd back to root */
+	ut_assertok(vfs_chdir("/"));
+	ut_asserteq_str("/", vfs_getcwd());
+
+	/* cd to non-existent path should fail */
+	ut_asserteq(-ENOENT, vfs_chdir("/nowhere"));
+	/* cwd should be unchanged */
+	ut_asserteq_str("/", vfs_getcwd());
+
+	ut_assertok(vfs_umount_path(vfs, "/host"));
+
+	return 0;
+}
+DM_TEST(dm_test_vfs_cwd, UTF_SCAN_FDT);
+
 #endif