[Concept,09/16] ext4l: Add open/read/close directory

Message ID 20251227204318.886983-10-sjg@u-boot.org
State New
Headers
Series fs: ext4l: Complete read-only filesystem support (Part I) |

Commit Message

Simon Glass Dec. 27, 2025, 8:43 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Implement directory-iteration for the ext4l filesystem driver, allowing
callers to iterate through directory entries one at a time.

Add ext4l_opendir() which opens a directory and returns a stream handle,
ext4l_readdir() which returns the next directory entry, and
ext4l_closedir() which closes the stream and frees resources.

The implementation uses a struct dir_context to capture single entries
from ext4_readdir(), with logic to skip previously returned entries
since the htree code may re-emit them.

Update struct file to include a position.

Wire these functions into fs_legacy.c for the ext4l filesystem type.

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 fs/ext4l/interface.c                | 210 ++++++++++++++++++++++++++++
 fs/fs_legacy.c                      |   4 +-
 include/ext4l.h                     |  28 ++++
 include/linux/fs.h                  |   1 +
 test/fs/ext4l.c                     |  85 +++++++++++
 test/py/tests/test_fs/test_ext4l.py |  22 ++-
 6 files changed, 348 insertions(+), 2 deletions(-)
  

Patch

diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c
index 6e146f246bd..e7f09fd45dc 100644
--- a/fs/ext4l/interface.c
+++ b/fs/ext4l/interface.c
@@ -11,6 +11,7 @@ 
 
 #include <blk.h>
 #include <env.h>
+#include <fs.h>
 #include <membuf.h>
 #include <part.h>
 #include <malloc.h>
@@ -33,6 +34,9 @@  static struct blk_desc *ext4l_blk_dev;
 static struct disk_partition ext4l_partition;
 static int ext4l_mounted;
 
+/* Count of open directory streams (prevents unmount while iterating) */
+static int ext4l_open_dirs;
+
 /* Global super_block pointer for filesystem operations */
 static struct super_block *ext4l_sb;
 
@@ -634,7 +638,213 @@  int ext4l_ls(const char *dirname)
 
 void ext4l_close(void)
 {
+	if (ext4l_open_dirs > 0)
+		return;
+
 	ext4l_dev_desc = NULL;
 	ext4l_sb = NULL;
 	ext4l_clear_blk_dev();
 }
+
+/**
+ * struct ext4l_dir - ext4l directory stream state
+ * @parent: base fs_dir_stream structure
+ * @dirent: directory entry to return to caller
+ * @dir_inode: pointer to directory inode
+ * @file: file structure for ext4_readdir
+ * @entry_found: flag set by actor when entry is captured
+ * @last_ino: inode number of last returned entry (to skip on next call)
+ * @skip_last: true if we need to skip the last_ino entry
+ *
+ * The filesystem stays mounted while directory streams are open (ext4l_close
+ * checks ext4l_open_dirs), so we can keep direct pointers to inodes.
+ */
+struct ext4l_dir {
+	struct fs_dir_stream parent;
+	struct fs_dirent dirent;
+	struct inode *dir_inode;
+	struct file file;
+	bool entry_found;
+	u64 last_ino;
+	bool skip_last;
+};
+
+/**
+ * struct ext4l_readdir_ctx - Extended dir_context with back-pointer
+ * @ctx: base dir_context structure (must be first)
+ * @dir: pointer to ext4l_dir for state updates
+ */
+struct ext4l_readdir_ctx {
+	struct dir_context ctx;
+	struct ext4l_dir *dir;
+};
+
+/**
+ * ext4l_opendir_actor() - dir_context actor that captures single entry
+ *
+ * This actor is called by ext4_readdir for each directory entry. It captures
+ * the first entry found (skipping the previously returned entry if needed)
+ * and returns non-zero to stop iteration.
+ */
+static int ext4l_opendir_actor(struct dir_context *ctx, const char *name,
+			       int namelen, loff_t offset, u64 ino,
+			       unsigned int d_type)
+{
+	struct ext4l_readdir_ctx *rctx;
+	struct ext4l_dir *dir;
+	struct fs_dirent *dent;
+	struct inode *inode;
+
+	rctx = container_of(ctx, struct ext4l_readdir_ctx, ctx);
+	dir = rctx->dir;
+
+	/*
+	 * Skip the entry we returned last time. The htree code may call us
+	 * with the same entry again due to its extra_fname handling.
+	 */
+	if (dir->skip_last && ino == dir->last_ino) {
+		dir->skip_last = false;
+		return 0;  /* Continue to next entry */
+	}
+
+	dent = &dir->dirent;
+
+	/* Copy name */
+	if (namelen >= FS_DIRENT_NAME_LEN)
+		namelen = FS_DIRENT_NAME_LEN - 1;
+	memcpy(dent->name, name, namelen);
+	dent->name[namelen] = '\0';
+
+	/* Set type based on d_type hint */
+	switch (d_type) {
+	case DT_DIR:
+		dent->type = FS_DT_DIR;
+		break;
+	case DT_LNK:
+		dent->type = FS_DT_LNK;
+		break;
+	default:
+		dent->type = FS_DT_REG;
+		break;
+	}
+
+	/* Look up inode to get size and other attributes */
+	inode = ext4_iget(ext4l_sb, ino, 0);
+	if (!IS_ERR(inode)) {
+		dent->size = inode->i_size;
+		/* Refine type from inode mode if needed */
+		if (S_ISDIR(inode->i_mode))
+			dent->type = FS_DT_DIR;
+		else if (S_ISLNK(inode->i_mode))
+			dent->type = FS_DT_LNK;
+		else
+			dent->type = FS_DT_REG;
+	} else {
+		dent->size = 0;
+	}
+
+	dir->entry_found = true;
+	dir->last_ino = ino;
+
+	/*
+	 * Return non-zero to stop iteration after one entry.
+	 * dir_emit() returns (actor(...) == 0), so:
+	 *   actor returns 0 -> dir_emit returns 1 (continue)
+	 *   actor returns non-zero -> dir_emit returns 0 (stop)
+	 */
+	return 1;
+}
+
+int ext4l_opendir(const char *filename, struct fs_dir_stream **dirsp)
+{
+	struct ext4l_dir *dir;
+	struct inode *inode;
+	int ret;
+
+	if (!ext4l_mounted)
+		return -ENODEV;
+
+	ret = ext4l_resolve_path(filename, &inode);
+	if (ret)
+		return ret;
+
+	if (!S_ISDIR(inode->i_mode))
+		return -ENOTDIR;
+
+	dir = calloc(1, sizeof(*dir));
+	if (!dir)
+		return -ENOMEM;
+
+	dir->dir_inode = inode;
+	dir->entry_found = false;
+
+	/* Set up file structure for ext4_readdir */
+	dir->file.f_inode = inode;
+	dir->file.f_mapping = inode->i_mapping;
+	dir->file.private_data = kzalloc(sizeof(struct dir_private_info),
+					 GFP_KERNEL);
+	if (!dir->file.private_data) {
+		free(dir);
+		return -ENOMEM;
+	}
+
+	/* Increment open dir count to prevent unmount */
+	ext4l_open_dirs++;
+
+	*dirsp = (struct fs_dir_stream *)dir;
+
+	return 0;
+}
+
+int ext4l_readdir(struct fs_dir_stream *dirs, struct fs_dirent **dentp)
+{
+	struct ext4l_dir *dir = (struct ext4l_dir *)dirs;
+	struct ext4l_readdir_ctx ctx;
+	int ret;
+
+	if (!ext4l_mounted)
+		return -ENODEV;
+
+	memset(&dir->dirent, '\0', sizeof(dir->dirent));
+	dir->entry_found = false;
+
+	/* Skip the entry we returned last time (htree may re-emit it) */
+	if (dir->last_ino)
+		dir->skip_last = true;
+
+	/* Set up extended dir_context for this iteration */
+	memset(&ctx, '\0', sizeof(ctx));
+	ctx.ctx.actor = ext4l_opendir_actor;
+	ctx.ctx.pos = dir->file.f_pos;
+	ctx.dir = dir;
+
+	ret = ext4_readdir(&dir->file, &ctx.ctx);
+
+	/* Update file position for next call */
+	dir->file.f_pos = ctx.ctx.pos;
+
+	if (ret < 0)
+		return ret;
+
+	if (!dir->entry_found)
+		return -ENOENT;
+
+	*dentp = &dir->dirent;
+
+	return 0;
+}
+
+void ext4l_closedir(struct fs_dir_stream *dirs)
+{
+	struct ext4l_dir *dir = (struct ext4l_dir *)dirs;
+
+	if (dir) {
+		if (dir->file.private_data)
+			ext4_htree_free_dir_info(dir->file.private_data);
+		free(dir);
+	}
+
+	/* Decrement open dir count */
+	if (ext4l_open_dirs > 0)
+		ext4l_open_dirs--;
+}
diff --git a/fs/fs_legacy.c b/fs/fs_legacy.c
index 6ca9d6e647a..7d293468ea8 100644
--- a/fs/fs_legacy.c
+++ b/fs/fs_legacy.c
@@ -271,7 +271,9 @@  static struct fstype_info fstypes[] = {
 		.read = fs_read_unsupported,
 		.write = fs_write_unsupported,
 		.uuid = fs_uuid_unsupported,
-		.opendir = fs_opendir_unsupported,
+		.opendir = ext4l_opendir,
+		.readdir = ext4l_readdir,
+		.closedir = ext4l_closedir,
 		.unlink = fs_unlink_unsupported,
 		.mkdir = fs_mkdir_unsupported,
 		.ln = fs_ln_unsupported,
diff --git a/include/ext4l.h b/include/ext4l.h
index 333d9db139c..6d8eba84f4e 100644
--- a/include/ext4l.h
+++ b/include/ext4l.h
@@ -11,6 +11,8 @@ 
 
 struct blk_desc;
 struct disk_partition;
+struct fs_dir_stream;
+struct fs_dirent;
 
 /**
  * ext4l_probe() - Probe a block device for an ext4 filesystem
@@ -44,4 +46,30 @@  int ext4l_ls(const char *dirname);
  */
 int ext4l_get_uuid(u8 *uuid);
 
+/**
+ * ext4l_opendir() - Open a directory for iteration
+ *
+ * @filename: Directory path
+ * @dirsp: Returns directory stream pointer
+ * Return: 0 on success, -ENODEV if not mounted, -ENOTDIR if not a directory,
+ *	   -ENOMEM on allocation failure
+ */
+int ext4l_opendir(const char *filename, struct fs_dir_stream **dirsp);
+
+/**
+ * ext4l_readdir() - Read the next directory entry
+ *
+ * @dirs: Directory stream from ext4l_opendir
+ * @dentp: Returns pointer to directory entry
+ * Return: 0 on success, -ENODEV if not mounted, -ENOENT at end of directory
+ */
+int ext4l_readdir(struct fs_dir_stream *dirs, struct fs_dirent **dentp);
+
+/**
+ * ext4l_closedir() - Close a directory stream
+ *
+ * @dirs: Directory stream to close
+ */
+void ext4l_closedir(struct fs_dir_stream *dirs);
+
 #endif /* __EXT4L_H__ */
diff --git a/include/linux/fs.h b/include/linux/fs.h
index ef28c12c022..090ee192061 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -98,6 +98,7 @@  struct file {
 	void *private_data;
 	struct file_ra_state f_ra;
 	struct path f_path;
+	loff_t f_pos;
 };
 
 /* Get inode from file */
diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c
index 4c477ce3338..d9ed21407e7 100644
--- a/test/fs/ext4l.c
+++ b/test/fs/ext4l.c
@@ -111,3 +111,88 @@  static int fs_test_ext4l_ls_norun(struct unit_test_state *uts)
 }
 FS_TEST_ARGS(fs_test_ext4l_ls_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL,
 	     { "fs_image", UT_ARG_STR });
+
+/**
+ * fs_test_ext4l_opendir_norun() - Test ext4l opendir/readdir/closedir
+ *
+ * Verifies that the ext4l driver can iterate through directory entries using
+ * the opendir/readdir/closedir interface. It checks:
+ * - Regular files (testfile.txt)
+ * - Subdirectories (subdir)
+ * - Symlinks (link.txt)
+ * - Files in subdirectories (subdir/nested.txt)
+ *
+ * Arguments:
+ *   fs_image: Path to the ext4 filesystem image
+ */
+static int fs_test_ext4l_opendir_norun(struct unit_test_state *uts)
+{
+	const char *fs_image = ut_str(EXT4L_ARG_IMAGE);
+	struct fs_dir_stream *dirs;
+	struct fs_dirent *dent;
+	bool found_testfile = false;
+	bool found_subdir = false;
+	bool found_symlink = false;
+	bool found_nested = false;
+	int count = 0;
+
+	ut_assertnonnull(fs_image);
+	ut_assertok(run_commandf("host bind 0 %s", fs_image));
+	ut_assertok(fs_set_blk_dev("host", "0", FS_TYPE_ANY));
+
+	/* Open root directory */
+	ut_assertok(ext4l_opendir("/", &dirs));
+	ut_assertnonnull(dirs);
+
+	/* Iterate through entries */
+	while (!ext4l_readdir(dirs, &dent)) {
+		ut_assertnonnull(dent);
+		count++;
+		if (!strcmp(dent->name, "testfile.txt")) {
+			found_testfile = true;
+			ut_asserteq(FS_DT_REG, dent->type);
+			ut_asserteq(12, dent->size);
+		} else if (!strcmp(dent->name, "subdir")) {
+			found_subdir = true;
+			ut_asserteq(FS_DT_DIR, dent->type);
+		} else if (!strcmp(dent->name, "link.txt")) {
+			found_symlink = true;
+			ut_asserteq(FS_DT_LNK, dent->type);
+		}
+	}
+
+	ext4l_closedir(dirs);
+
+	/* Verify we found expected entries */
+	ut_assert(found_testfile);
+	ut_assert(found_subdir);
+	ut_assert(found_symlink);
+	/* At least ., .., testfile.txt, subdir, link.txt */
+	ut_assert(count >= 5);
+
+	/* Now test reading the subdirectory */
+	ut_assertok(fs_set_blk_dev("host", "0", FS_TYPE_ANY));
+	ut_assertok(ext4l_opendir("/subdir", &dirs));
+	ut_assertnonnull(dirs);
+
+	count = 0;
+	while (!ext4l_readdir(dirs, &dent)) {
+		ut_assertnonnull(dent);
+		count++;
+		if (!strcmp(dent->name, "nested.txt")) {
+			found_nested = true;
+			ut_asserteq(FS_DT_REG, dent->type);
+			ut_asserteq(12, dent->size);
+		}
+	}
+
+	ext4l_closedir(dirs);
+
+	ut_assert(found_nested);
+	/* At least ., .., nested.txt */
+	ut_assert(count >= 3);
+
+	return 0;
+}
+FS_TEST_ARGS(fs_test_ext4l_opendir_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 93b6d4d34e8..073b02a80ce 100644
--- a/test/py/tests/test_fs/test_ext4l.py
+++ b/test/py/tests/test_fs/test_ext4l.py
@@ -37,14 +37,27 @@  class TestExt4l:
                        shell=True)
             check_call(f'mkfs.ext4 -q {image_path}', shell=True)
 
-            # Add a test file using debugfs (no mount required)
+            # Add test files using debugfs (no mount required)
             with NamedTemporaryFile(mode='w', delete=False) as tmp:
                 tmp.write('hello world\n')
                 tmp_path = tmp.name
             try:
+                # Add a regular file
                 check_call(f'debugfs -w {image_path} '
                            f'-R "write {tmp_path} testfile.txt" 2>/dev/null',
                            shell=True)
+                # Add a subdirectory
+                check_call(f'debugfs -w {image_path} '
+                           f'-R "mkdir subdir" 2>/dev/null',
+                           shell=True)
+                # Add a file in the subdirectory
+                check_call(f'debugfs -w {image_path} '
+                           f'-R "write {tmp_path} subdir/nested.txt" 2>/dev/null',
+                           shell=True)
+                # Add a symlink
+                check_call(f'debugfs -w {image_path} '
+                           f'-R "symlink link.txt testfile.txt" 2>/dev/null',
+                           shell=True)
             finally:
                 os.unlink(tmp_path)
         except CalledProcessError:
@@ -76,3 +89,10 @@  class TestExt4l:
             output = ubman.run_command(
                 f'ut -f fs fs_test_ext4l_ls_norun fs_image={ext4_image}')
             assert 'failures: 0' in output
+
+    def test_opendir(self, ubman, ext4_image):
+        """Test that ext4l can iterate directory entries."""
+        with ubman.log.section('Test ext4l opendir'):
+            output = ubman.run_command(
+                f'ut -f fs fs_test_ext4l_opendir_norun fs_image={ext4_image}')
+            assert 'failures: 0' in output