[Concept,12/12] ext4l: Add ls command support

Message ID 20251223011632.380026-13-sjg@u-boot.org
State New
Headers
Series ext4l: Add support for listing directoties (Part H) |

Commit Message

Simon Glass Dec. 23, 2025, 1:16 a.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Implement directory listing (ls) for the ext4l filesystem driver. This
includes path resolution with symlink following (limited to 8 levels to
prevent loops).

Add ext4l_ls() which uses ext4_lookup() for path resolution and
ext4_readdir() for directory enumeration. The dir_context actor callback
formats and prints each directory entry.

Export ext4_lookup() from namei.c and add declarations to ext4.h.

Add test_ls to the Python test suite, which creates a test file using
debugfs and verifies it appears in the directory listing with the
correct size.

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>

Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 fs/ext4l/ext4.h                     |   3 +
 fs/ext4l/interface.c                | 321 ++++++++++++++++++++++++++++
 fs/ext4l/namei.c                    |   2 +-
 fs/fs_legacy.c                      |   2 +-
 include/ext4l.h                     |   7 +
 test/fs/ext4l.c                     |  29 +++
 test/py/tests/test_fs/test_ext4l.py |  19 ++
 7 files changed, 381 insertions(+), 2 deletions(-)
  

Patch

diff --git a/fs/ext4l/ext4.h b/fs/ext4l/ext4.h
index b2f75437bbc..1c2d5beb121 100644
--- a/fs/ext4l/ext4.h
+++ b/fs/ext4l/ext4.h
@@ -2857,6 +2857,9 @@  extern int ext4_htree_store_dirent(struct file *dir_file, __u32 hash,
 				struct ext4_dir_entry_2 *dirent,
 				struct fscrypt_str *ent_name);
 extern void ext4_htree_free_dir_info(struct dir_private_info *p);
+extern int ext4_readdir(struct file *file, struct dir_context *ctx);
+extern struct dentry *ext4_lookup(struct inode *dir, struct dentry *dentry,
+				  unsigned int flags);
 extern int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
 			     void *buf, int buf_size,
 			     struct ext4_filename *fname,
diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c
index 638f51d8c64..b897f30c223 100644
--- a/fs/ext4l/interface.c
+++ b/fs/ext4l/interface.c
@@ -302,6 +302,327 @@  err_exit_es:
 	return ret;
 }
 
+/**
+ * ext4l_read_symlink() - Read the target of a symlink inode
+ * @inode: Symlink inode
+ * @target: Buffer to store target
+ * @max_len: Maximum length of target buffer
+ * Return: Length of target on success, negative on error
+ */
+static int ext4l_read_symlink(struct inode *inode, char *target, size_t max_len)
+{
+	struct buffer_head *bh;
+	size_t len;
+
+	if (!S_ISLNK(inode->i_mode))
+		return -EINVAL;
+
+	if (ext4_inode_is_fast_symlink(inode)) {
+		/* Fast symlink: target stored in i_data */
+		len = inode->i_size;
+		if (len >= max_len)
+			len = max_len - 1;
+		memcpy(target, EXT4_I(inode)->i_data, len);
+		target[len] = '\0';
+		return len;
+	}
+
+	/* Slow symlink: target stored in data block */
+	bh = ext4_bread(NULL, inode, 0, 0);
+	if (IS_ERR(bh))
+		return PTR_ERR(bh);
+	if (!bh)
+		return -EIO;
+
+	len = inode->i_size;
+	if (len >= max_len)
+		len = max_len - 1;
+	memcpy(target, bh->b_data, len);
+	target[len] = '\0';
+	brelse(bh);
+
+	return len;
+}
+
+/* Forward declaration for recursive resolution */
+static int ext4l_resolve_path_internal(const char *path, struct inode **inodep,
+				       int depth);
+
+/**
+ * ext4l_resolve_path() - Resolve path to inode
+ * @path: Path to resolve
+ * @inodep: Output inode pointer
+ * Return: 0 on success, negative on error
+ */
+static int ext4l_resolve_path(const char *path, struct inode **inodep)
+{
+	return ext4l_resolve_path_internal(path, inodep, 0);
+}
+
+/**
+ * ext4l_resolve_path_internal() - Resolve path with symlink following
+ * @path: Path to resolve
+ * @inodep: Output inode pointer
+ * @depth: Current recursion depth (for symlink loop detection)
+ * Return: 0 on success, negative on error
+ */
+static int ext4l_resolve_path_internal(const char *path, struct inode **inodep,
+				       int depth)
+{
+	struct inode *dir;
+	struct dentry *dentry, *result;
+	char *path_copy, *component, *next_component;
+	int ret;
+
+	/* Prevent symlink loops */
+	if (depth > 8)
+		return -ELOOP;
+
+	if (!ext4l_mounted) {
+		ext4_debug("ext4l_resolve_path: filesystem not mounted\n");
+		return -ENODEV;
+	}
+
+	dir = ext4l_sb->s_root->d_inode;
+
+	if (!path || !*path || (strcmp(path, "/") == 0)) {
+		*inodep = dir;
+		return 0;
+	}
+
+	path_copy = strdup(path);
+	if (!path_copy)
+		return -ENOMEM;
+
+	component = path_copy;
+	/* Skip leading slash */
+	if (*component == '/')
+		component++;
+
+	while (component && *component) {
+		next_component = strchr(component, '/');
+		if (next_component) {
+			*next_component = '\0';
+			next_component++;
+		}
+
+		if (!*component) {
+			component = next_component;
+			continue;
+		}
+
+		/* Handle special directory entries */
+		if (strcmp(component, ".") == 0) {
+			component = next_component;
+			continue;
+		}
+		if (strcmp(component, "..") == 0) {
+			/* Parent directory - look up ".." entry */
+			dentry = kzalloc(sizeof(struct dentry), GFP_KERNEL);
+			if (!dentry) {
+				free(path_copy);
+				return -ENOMEM;
+			}
+			dentry->d_name.name = "..";
+			dentry->d_name.len = 2;
+			dentry->d_sb = ext4l_sb;
+			dentry->d_parent = NULL;
+
+			result = ext4_lookup(dir, dentry, 0);
+			if (IS_ERR(result)) {
+				kfree(dentry);
+				free(path_copy);
+				return PTR_ERR(result);
+			}
+			if (result && result->d_inode) {
+				dir = result->d_inode;
+				if (result != dentry)
+					kfree(dentry);
+				kfree(result);
+			} else if (dentry->d_inode) {
+				dir = dentry->d_inode;
+				kfree(dentry);
+			} else {
+				/* ".." not found - stay at root */
+				kfree(dentry);
+				if (result && result != dentry)
+					kfree(result);
+			}
+			component = next_component;
+			continue;
+		}
+
+		dentry = kzalloc(sizeof(struct dentry), GFP_KERNEL);
+		if (!dentry) {
+			free(path_copy);
+			return -ENOMEM;
+		}
+
+		dentry->d_name.name = component;
+		dentry->d_name.len = strlen(component);
+		dentry->d_sb = ext4l_sb;
+		dentry->d_parent = NULL;
+
+		result = ext4_lookup(dir, dentry, 0);
+
+		if (IS_ERR(result)) {
+			kfree(dentry);
+			free(path_copy);
+			return PTR_ERR(result);
+		}
+
+		if (result) {
+			if (!result->d_inode) {
+				if (result != dentry)
+					kfree(dentry);
+				kfree(result);
+				free(path_copy);
+				return -ENOENT;
+			}
+			dir = result->d_inode;
+			if (result != dentry)
+				kfree(dentry);
+			kfree(result);
+		} else {
+			if (!dentry->d_inode) {
+				kfree(dentry);
+				free(path_copy);
+				return -ENOENT;
+			}
+			dir = dentry->d_inode;
+			kfree(dentry);
+		}
+
+		if (!dir) {
+			free(path_copy);
+			return -ENOENT;
+		}
+
+		/* Check if this is a symlink and follow it */
+		if (S_ISLNK(dir->i_mode)) {
+			char link_target[256];
+			char *new_path;
+
+			ret = ext4l_read_symlink(dir, link_target,
+						 sizeof(link_target));
+			if (ret < 0) {
+				free(path_copy);
+				return ret;
+			}
+
+			/* Build new path: link_target + remaining path */
+			if (next_component && *next_component) {
+				size_t target_len = strlen(link_target);
+				size_t remaining_len = strlen(next_component);
+
+				new_path = malloc(target_len + 1 +
+						  remaining_len + 1);
+				if (!new_path) {
+					free(path_copy);
+					return -ENOMEM;
+				}
+				strcpy(new_path, link_target);
+				strcat(new_path, "/");
+				strcat(new_path, next_component);
+			} else {
+				new_path = strdup(link_target);
+				if (!new_path) {
+					free(path_copy);
+					return -ENOMEM;
+				}
+			}
+
+			free(path_copy);
+
+			/* Recursively resolve the new path */
+			ret = ext4l_resolve_path_internal(new_path, inodep,
+							  depth + 1);
+			free(new_path);
+			return ret;
+		}
+
+		component = next_component;
+	}
+
+	free(path_copy);
+	*inodep = dir;
+	return 0;
+}
+
+/**
+ * ext4l_dir_actor() - Directory entry callback for ext4_readdir
+ * @ctx: Directory context
+ * @name: Entry name
+ * @namelen: Length of name
+ * @offset: Directory offset
+ * @ino: Inode number
+ * @d_type: Entry type
+ * Return: 0 to continue iteration
+ */
+static int ext4l_dir_actor(struct dir_context *ctx, const char *name,
+			   int namelen, loff_t offset, u64 ino,
+			   unsigned int d_type)
+{
+	struct inode *inode;
+	char namebuf[256];
+
+	/* Copy the name to a null-terminated buffer */
+	if (namelen >= sizeof(namebuf))
+		namelen = sizeof(namebuf) - 1;
+	memcpy(namebuf, name, namelen);
+	namebuf[namelen] = '\0';
+
+	/* Look up the inode to get file size */
+	inode = ext4_iget(ext4l_sb, ino, 0);
+	if (IS_ERR(inode)) {
+		printf(" %8s   %s\n", "?", namebuf);
+		return 0;
+	}
+
+	if (d_type == DT_DIR || S_ISDIR(inode->i_mode))
+		printf("            %s/\n", namebuf);
+	else if (d_type == DT_LNK || S_ISLNK(inode->i_mode))
+		printf("    <SYM>   %s\n", namebuf);
+	else
+		printf(" %8lld   %s\n", (long long)inode->i_size, namebuf);
+
+	return 0;
+}
+
+int ext4l_ls(const char *dirname)
+{
+	struct inode *dir;
+	struct file file;
+	struct dir_context ctx;
+	int ret;
+
+	ret = ext4l_resolve_path(dirname, &dir);
+	if (ret)
+		return ret;
+
+	if (!S_ISDIR(dir->i_mode))
+		return -ENOTDIR;
+
+	memset(&file, 0, sizeof(file));
+	file.f_inode = dir;
+	file.f_mapping = dir->i_mapping;
+
+	/* Allocate private_data for readdir */
+	file.private_data = kzalloc(sizeof(struct dir_private_info), GFP_KERNEL);
+	if (!file.private_data)
+		return -ENOMEM;
+
+	memset(&ctx, 0, sizeof(ctx));
+	ctx.actor = ext4l_dir_actor;
+
+	ret = ext4_readdir(&file, &ctx);
+
+	if (file.private_data)
+		ext4_htree_free_dir_info(file.private_data);
+
+	return ret;
+}
+
 void ext4l_close(void)
 {
 	ext4l_dev_desc = NULL;
diff --git a/fs/ext4l/namei.c b/fs/ext4l/namei.c
index 7ef20d02235..53c48d12918 100644
--- a/fs/ext4l/namei.c
+++ b/fs/ext4l/namei.c
@@ -1746,7 +1746,7 @@  success:
 	return bh;
 }
 
-static struct dentry *ext4_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags)
+struct dentry *ext4_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags)
 {
 	struct inode *inode;
 	struct ext4_dir_entry_2 *de;
diff --git a/fs/fs_legacy.c b/fs/fs_legacy.c
index 5b96e1465d8..29b3ee83922 100644
--- a/fs/fs_legacy.c
+++ b/fs/fs_legacy.c
@@ -265,7 +265,7 @@  static struct fstype_info fstypes[] = {
 		.null_dev_desc_ok = false,
 		.probe = ext4l_probe,
 		.close = ext4l_close,
-		.ls = fs_ls_unsupported,
+		.ls = ext4l_ls,
 		.exists = fs_exists_unsupported,
 		.size = fs_size_unsupported,
 		.read = fs_read_unsupported,
diff --git a/include/ext4l.h b/include/ext4l.h
index dead8ba8e6f..e6ca11c163a 100644
--- a/include/ext4l.h
+++ b/include/ext4l.h
@@ -28,6 +28,13 @@  int ext4l_probe(struct blk_desc *fs_dev_desc,
  */
 void ext4l_close(void);
 
+/**
+ * ext4l_ls() - List directory contents
+ * @dirname: Directory path to list
+ * Return: 0 on success, negative on error
+ */
+int ext4l_ls(const char *dirname);
+
 /**
  * ext4l_get_uuid() - Get the filesystem UUID
  * @uuid: Buffer to receive the 16-byte UUID
diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c
index e566c9e97b0..122b022d8d8 100644
--- a/test/fs/ext4l.c
+++ b/test/fs/ext4l.c
@@ -79,3 +79,32 @@  static int fs_test_ext4l_msgs_norun(struct unit_test_state *uts)
 }
 FS_TEST_ARGS(fs_test_ext4l_msgs_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL,
 	     { "fs_image", UT_ARG_STR });
+
+/**
+ * fs_test_ext4l_ls_norun() - Test ext4l ls command
+ *
+ * This test verifies that the ext4l driver can list directory contents.
+ *
+ * Arguments:
+ *   fs_image: Path to the ext4 filesystem image
+ */
+static int fs_test_ext4l_ls_norun(struct unit_test_state *uts)
+{
+	const char *fs_image = ut_str(EXT4L_ARG_IMAGE);
+
+	ut_assertnonnull(fs_image);
+	ut_assertok(run_commandf("host bind 0 %s", fs_image));
+	console_record_reset_enable();
+	ut_assertok(run_commandf("ls host 0"));
+	/*
+	 * The Python test adds testfile.txt (12 bytes) to the image.
+	 * Directory entries appear in hash order which varies between runs.
+	 * Verify the file entry appears with correct size (12 bytes).
+	 */
+	ut_assert_skip_to_line("       12   testfile.txt");
+	ut_assert_console_end();
+
+	return 0;
+}
+FS_TEST_ARGS(fs_test_ext4l_ls_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL,
+	     { "fs_image", UT_ARG_STR });
diff --git a/test/py/tests/test_fs/test_ext4l.py b/test/py/tests/test_fs/test_ext4l.py
index 3b206293cbc..93b6d4d34e8 100644
--- a/test/py/tests/test_fs/test_ext4l.py
+++ b/test/py/tests/test_fs/test_ext4l.py
@@ -10,6 +10,7 @@  Test ext4l filesystem probing via C unit test.
 
 import os
 from subprocess import CalledProcessError, check_call
+from tempfile import NamedTemporaryFile
 
 import pytest
 
@@ -35,6 +36,17 @@  class TestExt4l:
             check_call(f'dd if=/dev/zero of={image_path} bs=1M count=64 2>/dev/null',
                        shell=True)
             check_call(f'mkfs.ext4 -q {image_path}', shell=True)
+
+            # Add a test file using debugfs (no mount required)
+            with NamedTemporaryFile(mode='w', delete=False) as tmp:
+                tmp.write('hello world\n')
+                tmp_path = tmp.name
+            try:
+                check_call(f'debugfs -w {image_path} '
+                           f'-R "write {tmp_path} testfile.txt" 2>/dev/null',
+                           shell=True)
+            finally:
+                os.unlink(tmp_path)
         except CalledProcessError:
             pytest.skip('Failed to create ext4 image')
 
@@ -57,3 +69,10 @@  class TestExt4l:
             output = ubman.run_command(
                 f'ut -f fs fs_test_ext4l_msgs_norun fs_image={ext4_image}')
             assert 'failures: 0' in output
+
+    def test_ls(self, ubman, ext4_image):
+        """Test that ext4l can list directory contents."""
+        with ubman.log.section('Test ext4l ls'):
+            output = ubman.run_command(
+                f'ut -f fs fs_test_ext4l_ls_norun fs_image={ext4_image}')
+            assert 'failures: 0' in output