From patchwork Sat Dec 27 20:43:04 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 1086 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1766868272; bh=I1Sdv7AmN9Yrk97qH9Otxdw+f8fqB/srONW60IjhRYI=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=XIUJ6UzyUQEyHOby5NDMjlFR6bKh7BYIPs7CnpaZA0ar+wZJgjXQXcLSFuangMVp7 6aEUj1xemgBbz957WYCD0cC5QIOCRGZgvWfHFctnmdabgIFjz21MctMTmQTe9V0J+7 OiyslHJCMD6etLlRBmN/3uM7dN7dZVr/F9I0HpZ8AWEoJRfxvVaGjPFBHYLfmfjt/4 +g8fbmhFK93kdPsRQMiOF7cFREZuSqEcMP+ryIgyUFPj/MEtWwfkCY33v+LXL0GfJ8 NDYr9On1Ypf7g+oFAnWzyaB1vQRHKiYgO0uTq+Z6phdYQUGw/J1whPh1SWpNar5F4g zXZ1Dua2M30NQ== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 3828D64E49 for ; Sat, 27 Dec 2025 13:44:32 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id OZ7yg73Nok3O for ; Sat, 27 Dec 2025 13:44:32 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1766868272; bh=I1Sdv7AmN9Yrk97qH9Otxdw+f8fqB/srONW60IjhRYI=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=XIUJ6UzyUQEyHOby5NDMjlFR6bKh7BYIPs7CnpaZA0ar+wZJgjXQXcLSFuangMVp7 6aEUj1xemgBbz957WYCD0cC5QIOCRGZgvWfHFctnmdabgIFjz21MctMTmQTe9V0J+7 OiyslHJCMD6etLlRBmN/3uM7dN7dZVr/F9I0HpZ8AWEoJRfxvVaGjPFBHYLfmfjt/4 +g8fbmhFK93kdPsRQMiOF7cFREZuSqEcMP+ryIgyUFPj/MEtWwfkCY33v+LXL0GfJ8 NDYr9On1Ypf7g+oFAnWzyaB1vQRHKiYgO0uTq+Z6phdYQUGw/J1whPh1SWpNar5F4g zXZ1Dua2M30NQ== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 26C3064E4D for ; Sat, 27 Dec 2025 13:44:32 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1766868269; bh=CjU0JtGKzFyRqKev/xMVnhuLuKcSFAsIrE3gvPO4a1M=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=UpBBmQAT20PBBSHVjIj916jIAztc7xWDkYU0IXaRZ+9MpzCuzQPZAjzJ4i2HmtTIY /Wt8RjmPgY30Bcoj4eepGXD7oesg8QXPeKCvgvUA1/88KqN8dRygGR93Vmb+Ypm/lc nMZhNeBPMwNmf/E9gzhZmjV6oU3TVFzDBFTh+rjBoMz2QOk4/435agS+lEJK5hXMiv LdSbzE9uBIK/LQaD0mDEek0No/TFWxDdleSnsp//cbvYmJ3CZrCY+5pZjmS/6Qcktw 0+D2OnF/kh6aeO07vuUPAtMHvE3JQJVh2J2I3LM0O6gTMzRbj8coBN9+l4OUu3pc9W GvnL1LDGjAzDA== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 52B1C64E42; Sat, 27 Dec 2025 13:44:29 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id I1VdwUaJIDpo; Sat, 27 Dec 2025 13:44:29 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1766868265; bh=zjqMJk+UxXGkfsjGwD8a27zTheXlbTj9IWAh9ezcvVI=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=gyel29xLu1r4tY4kXLyWO4FYfctxBDGX12d4E2DDCCiPFP/6w+Hx7A6uEIvvKZZVt v1MtWBWcKNMeM9/o1Vzn1dKk+hCjaJXCIB3fMfqPMiJYUzDiqrrbaUq2H/IeBt6ZMm hiN8ExDQqoHSSipDpgS4W9dwgXfE39/AIUnCfnW/3/32EKb/KgyaiIL209nwb3NpUU cMYZD7c/mn+SNJqv3DXzAiCBztX+2OWBuhULtY+ePNtm92htZ8cwFe4JjtfFp8tuqS x3wYSeUwHeMCeDcDu/euWdodT3om8h7MQhjK0wE9VYO2xZNFY9fm8NqZnmSKj0GQS5 /g24R2nrZG2oA== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id B1F9564E1F; Sat, 27 Dec 2025 13:44:24 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Sat, 27 Dec 2025 13:43:04 -0700 Message-ID: <20251227204318.886983-10-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251227204318.886983-1-sjg@u-boot.org> References: <20251227204318.886983-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: NOYYZEFR24U66EENLAYY3BXUF3Z4354M X-Message-ID-Hash: NOYYZEFR24U66EENLAYY3BXUF3Z4354M X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Heinrich Schuchardt , Simon Glass , "Claude Opus 4 . 5" X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 09/16] ext4l: Add open/read/close directory List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass 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 Signed-off-by: Simon Glass --- 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(-) 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 #include +#include #include #include #include @@ -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