@@ -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,
@@ -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;
@@ -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;
@@ -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,
@@ -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
@@ -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 });
@@ -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