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(-) 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 -- 2.43.0