[PATCH 00/12] ext4l: Add support for listing directoties (Part H)
From: Simon Glass <simon.glass@canonical.com> This series adds directory-listing support to the ext4l filesystem driver. It exports a few required functions from the Linux ext4 code, fixes the dir_emit() stub to properly call the actor callback, and implements ext4l_ls() with path resolution and symlink following. Simon Glass (12): ext4l: Fix format warning in mballoc.c fs: ext4l: Mount filesystems read-write ext4l: Add write support and buffer mapping for mount ext4l: Use EXT4L_PRINTF to enable ext4 diagnostic messages ext4l: Document __CHAR_UNSIGNED__ behavior for directory hashing ext4l: Implement super_set_uuid() to display filesystem UUID ext4l: Add message buffer to record ext4 messages ext4l: Add ext4l_msgs env var to print messages on mount test: fs: Add ext4l filesystem tests ext4l: Export ext4_readdir() for directory listing ext4l: Fix dir_emit() to call the actor callback ext4l: Add ls command support fs/ext4l/dir.c | 2 +- fs/ext4l/ext4.h | 5 +- fs/ext4l/ext4_uboot.h | 53 +++- fs/ext4l/interface.c | 393 ++++++++++++++++++++++++++++ fs/ext4l/mballoc.c | 4 +- fs/ext4l/namei.c | 2 +- fs/ext4l/super.c | 28 +- fs/ext4l/support.c | 55 +++- fs/fs_legacy.c | 2 +- include/ext4l.h | 14 + test/fs/Makefile | 1 + test/fs/ext4l.c | 110 ++++++++ test/py/tests/test_fs/test_ext4l.py | 78 ++++++ 13 files changed, 722 insertions(+), 25 deletions(-) create mode 100644 test/fs/ext4l.c create mode 100644 test/py/tests/test_fs/test_ext4l.py -- 2.43.0 base-commit: 986b6352c345adfbe7d93fd7b8b86ed1fa198c6a branch: exth
From: Simon Glass <simon.glass@canonical.com> Fix format warning: atomic64_t.counter is 'long' on 32-bit systems, so cast to 'long long' for %lld format. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/mballoc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ext4l/mballoc.c b/fs/ext4l/mballoc.c index b58d72dfd26..6b0926a597b 100644 --- a/fs/ext4l/mballoc.c +++ b/fs/ext4l/mballoc.c @@ -3907,9 +3907,9 @@ void ext4_mb_release(struct super_block *sb) atomic_read(&sbi->s_bal_breaks), atomic_read(&sbi->s_mb_lost_chunks)); ext4_msg(sb, KERN_INFO, - "mballoc: %u generated and it took %llu", + "mballoc: %u generated and it took %lld", atomic_read(&sbi->s_mb_buddies_generated), - atomic64_read(&sbi->s_mb_generation_time)); + (long long)atomic64_read(&sbi->s_mb_generation_time)); ext4_msg(sb, KERN_INFO, "mballoc: %u preallocated, %u discarded", atomic_read(&sbi->s_mb_preallocated), -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Change sb_rdonly() to return 0 to allow read-write mounting. This enables the filesystem to be mounted without the read-only restriction. Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 209f288f281..4dd03494586 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -289,8 +289,8 @@ extern struct user_namespace init_user_ns; /* might_sleep - stub */ #define might_sleep() do { } while (0) -/* sb_rdonly - for now U-Boot mounts filesystems read-only */ -#define sb_rdonly(sb) 1 +/* sb_rdonly - U-Boot mounts filesystems read-write */ +#define sb_rdonly(sb) 0 /* Trace stubs */ #define trace_ext4_journal_start_inode(...) do { } while (0) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add ext4l_write_block() to enable writing blocks to disk, which is needed for ext4_commit_super() during read-write mounts. Also update submit_bh() to call the write function and return proper error codes. Set the BH_Mapped flag in sb_getblk() and bdev_getblk() since ext4 checks this flag in ext4_commit_super() to verify the buffer is valid. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 2 +- fs/ext4l/support.c | 55 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 4dd03494586..6dd498c8f52 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -2150,7 +2150,7 @@ struct blockgroup_lock { }; /* Buffer submission stubs - declarations for stub.c implementations */ -void submit_bh(int op_flags, struct buffer_head *bh); +int submit_bh(int op_flags, struct buffer_head *bh); struct buffer_head *bdev_getblk(struct block_device *bdev, sector_t block, unsigned int size, gfp_t gfp); int trylock_buffer(struct buffer_head *bh); diff --git a/fs/ext4l/support.c b/fs/ext4l/support.c index 9e93426b565..05efa8d067c 100644 --- a/fs/ext4l/support.c +++ b/fs/ext4l/support.c @@ -352,6 +352,40 @@ int ext4l_read_block(sector_t block, size_t size, void *buffer) return 0; } +/** + * ext4l_write_block() - Write a block to the block device + * @block: Block number (filesystem block, not sector) + * @size: Block size in bytes + * @buffer: Source buffer + * Return: 0 on success, negative on error + */ +int ext4l_write_block(sector_t block, size_t size, void *buffer) +{ + struct blk_desc *blk_dev; + struct disk_partition *part; + lbaint_t sector; + lbaint_t sector_count; + unsigned long n; + + blk_dev = ext4l_get_blk_dev(); + part = ext4l_get_partition(); + if (!blk_dev) + return -EIO; + + /* Convert block to sector */ + sector = (block * size) / blk_dev->blksz + part->start; + sector_count = size / blk_dev->blksz; + + if (sector_count == 0) + sector_count = 1; + + n = blk_dwrite(blk_dev, sector, sector_count, buffer); + if (n != sector_count) + return -EIO; + + return 0; +} + /** * sb_getblk() - Get a buffer, using cache if available * @sb: Super block @@ -379,6 +413,9 @@ struct buffer_head *sb_getblk(struct super_block *sb, sector_t block) bh->b_bdev = sb->s_bdev; bh->b_size = sb->s_blocksize; + /* Mark buffer as having a valid disk mapping */ + set_buffer_mapped(bh); + /* Don't read - just allocate with zeroed data */ memset(bh->b_data, '\0', bh->b_size); @@ -474,6 +511,9 @@ struct buffer_head *bdev_getblk(struct block_device *bdev, sector_t block, bh->b_bdev = bdev; bh->b_size = size; + /* Mark buffer as having a valid disk mapping */ + set_buffer_mapped(bh); + /* Don't read - just allocate with zeroed data */ memset(bh->b_data, 0, bh->b_size); @@ -520,8 +560,9 @@ struct buffer_head *__bread(struct block_device *bdev, sector_t block, * submit_bh() - Submit a buffer_head for I/O * @op: Operation (REQ_OP_READ, REQ_OP_WRITE, etc.) * @bh: Buffer head to submit + * Return: 0 on success, negative on error */ -void submit_bh(int op, struct buffer_head *bh) +int submit_bh(int op, struct buffer_head *bh) { int ret; int op_type = op & 0xff; /* Mask out flags, keep operation type */ @@ -530,13 +571,19 @@ void submit_bh(int op, struct buffer_head *bh) ret = ext4l_read_block(bh->b_blocknr, bh->b_size, bh->b_data); if (ret) { clear_buffer_uptodate(bh); - return; + return ret; } set_buffer_uptodate(bh); } else if (op_type == REQ_OP_WRITE) { - /* Write support not implemented yet */ - clear_buffer_uptodate(bh); + ret = ext4l_write_block(bh->b_blocknr, bh->b_size, bh->b_data); + if (ret) { + clear_buffer_uptodate(bh); + return ret; + } + /* Mark buffer as clean (not dirty) after write */ } + + return 0; } /** -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Enable EXT4L_PRINTF when CONFIG_EXT4L_DEBUG is set so that ext4_msg() and other diagnostic macros print full messages instead of empty strings. Use EXT4L_PRINTF instead of CONFIG_PRINTK since U-Boot requires CONFIG_ options to be defined in Kconfig. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4.h | 2 +- fs/ext4l/ext4_uboot.h | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/fs/ext4l/ext4.h b/fs/ext4l/ext4.h index 5c37fcd76f9..b2f75437bbc 100644 --- a/fs/ext4l/ext4.h +++ b/fs/ext4l/ext4.h @@ -3237,7 +3237,7 @@ void __ext4_grp_locked_error(const char *, unsigned int, #define ext4_abort(sb, err, fmt, a...) \ __ext4_error((sb), __func__, __LINE__, true, (err), 0, (fmt), ## a) -#ifdef CONFIG_PRINTK +#ifdef EXT4L_PRINTF #define ext4_error_inode(inode, func, line, block, fmt, ...) \ __ext4_error_inode(inode, func, line, block, 0, fmt, ##__VA_ARGS__) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 6dd498c8f52..5c6e65b6e76 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -37,6 +37,17 @@ #include <linux/rbtree.h> /* Real rbtree implementation */ #include <u-boot/crc.h> /* For crc32() used by crc32_be */ +/* + * Enable ext4_msg() and other diagnostic macros to print full messages. + * This is needed for CONFIG_EXT4L_DEBUG to show useful error messages. + * + * Use EXT4L_PRINTF instead of CONFIG_PRINTK since U-Boot requires CONFIG_ + * options to be defined in Kconfig. + */ +#ifdef CONFIG_EXT4L_DEBUG +#define EXT4L_PRINTF 1 +#endif + /* * Override no_printk to avoid format warnings in disabled debug prints. * The Linux kernel uses sector_t as u64, but U-Boot uses unsigned long. -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add documentation explaining how ext4's directory hash algorithm selection works based on the platform's char signedness. GCC automatically defines __CHAR_UNSIGNED__ on platforms where char is unsigned (e.g., ARM), which affects the hash algorithm used. The filesystem stores the hash variant in the superblock flags, ensuring correct operation regardless of the mounting platform. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 5c6e65b6e76..4b7b5d02c82 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -48,6 +48,22 @@ #define EXT4L_PRINTF 1 #endif +/* + * __CHAR_UNSIGNED__ - directory hash algorithm selection + * + * The ext4 filesystem uses different hash algorithms for directory indexing + * depending on whether the platform's 'char' type is signed or unsigned. + * GCC automatically defines __CHAR_UNSIGNED__ on platforms where char is + * unsigned (e.g., ARM), and leaves it undefined where char is signed + * (e.g., x86/sandbox). + * + * The filesystem stores EXT2_FLAGS_UNSIGNED_HASH or EXT2_FLAGS_SIGNED_HASH + * in the superblock to record which hash variant was used when the filesystem + * was created, ensuring correct behavior regardless of the mounting platform. + * + * See super.c:5123 and ioctl.c:1489 for the hash algorithm selection code. + */ + /* * Override no_printk to avoid format warnings in disabled debug prints. * The Linux kernel uses sector_t as u64, but U-Boot uses unsigned long. -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The super_set_uuid() function was stubbed as a no-op, causing the filesystem UUID to display as all zeros during mount. Implement it to copy the UUID from the ext4 superblock to the VFS super_block structure. Before: mounted filesystem 00000000-0000-0000-0000-000000000000 r/w After: mounted filesystem d4ddb235-df85-4963-a923-7cddc5ad9355 r/w Add a way to read from tests as well. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 11 +++++++++-- fs/ext4l/interface.c | 13 +++++++++++++ include/ext4l.h | 7 +++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 4b7b5d02c82..4b019b77d27 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -2324,8 +2324,15 @@ unsigned int bdev_max_discard_sectors(struct block_device *bdev); /* Task I/O priority - declaration for stub.c */ void set_task_ioprio(void *task, int ioprio); -/* Superblock identity stubs */ -#define super_set_uuid(sb, uuid, len) do { } while (0) +/* Superblock identity functions */ +static inline void super_set_uuid(struct super_block *sb, const u8 *uuid, + unsigned len) +{ + if (len > sizeof(sb->s_uuid.b)) + len = sizeof(sb->s_uuid.b); + memcpy(sb->s_uuid.b, uuid, len); +} + #define super_set_sysfs_name_bdev(sb) do { } while (0) /* diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 7360f44c92b..2ea3915c637 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -51,6 +51,19 @@ struct disk_partition *ext4l_get_partition(void) return &ext4l_partition; } +/** + * ext4l_get_uuid() - Get the filesystem UUID + * @uuid: Buffer to receive the 16-byte UUID + * Return: 0 on success, -ENODEV if not mounted + */ +int ext4l_get_uuid(u8 *uuid) +{ + if (!ext4l_sb) + return -ENODEV; + memcpy(uuid, ext4l_sb->s_uuid.b, 16); + return 0; +} + /** * ext4l_set_blk_dev() - Set the block device for ext4l operations * @blk_dev: Block device descriptor diff --git a/include/ext4l.h b/include/ext4l.h index 5a300fd6559..dead8ba8e6f 100644 --- a/include/ext4l.h +++ b/include/ext4l.h @@ -28,4 +28,11 @@ int ext4l_probe(struct blk_desc *fs_dev_desc, */ void ext4l_close(void); +/** + * ext4l_get_uuid() - Get the filesystem UUID + * @uuid: Buffer to receive the 16-byte UUID + * Return: 0 on success, -ENODEV if not mounted + */ +int ext4l_get_uuid(u8 *uuid); + #endif /* __EXT4L_H__ */ -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a 4KB membuf to record ext4 messages for later retrieval. This allows programmatic access to filesystem messages without requiring CONFIG_EXT4L_DEBUG to be enabled. - Add ext4l_msg_buf and ext4l_msg_data in interface.c - Add ext4l_record_msg() to record messages - Add ext4l_get_msg_buf() to access the buffer - Modify __ext4_msg() to always record messages, print only if debug Note that this increases the number of strings in the ext4l implementation. For production it might be useful to make this an option, although the messages can be quite important. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 9 +++++---- fs/ext4l/interface.c | 39 +++++++++++++++++++++++++++++++++++++++ fs/ext4l/super.c | 28 ++++++++++++++++++++-------- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 4b019b77d27..116f0475217 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -38,15 +38,14 @@ #include <u-boot/crc.h> /* For crc32() used by crc32_be */ /* - * Enable ext4_msg() and other diagnostic macros to print full messages. - * This is needed for CONFIG_EXT4L_DEBUG to show useful error messages. + * Enable ext4_msg() and other diagnostic macros to pass full messages. + * This is required for message recording to work. Without this, the + * ext4_msg macro passes empty strings to __ext4_msg(). * * Use EXT4L_PRINTF instead of CONFIG_PRINTK since U-Boot requires CONFIG_ * options to be defined in Kconfig. */ -#ifdef CONFIG_EXT4L_DEBUG #define EXT4L_PRINTF 1 -#endif /* * __CHAR_UNSIGNED__ - directory hash algorithm selection @@ -2864,6 +2863,8 @@ int ext4l_read_block(sector_t block, size_t size, void *buffer); /* ext4l interface functions (interface.c) */ struct blk_desc *ext4l_get_blk_dev(void); struct disk_partition *ext4l_get_partition(void); +void ext4l_record_msg(const char *msg, int len); +struct membuf *ext4l_get_msg_buf(void); #define sb_is_blkdev_sb(sb) ({ (void)(sb); 0; }) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 2ea3915c637..9b6f4c0b02b 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -10,6 +10,7 @@ */ #include <blk.h> +#include <membuf.h> #include <part.h> #include <malloc.h> #include <linux/errno.h> @@ -19,6 +20,9 @@ #include "ext4_uboot.h" #include "ext4.h" +/* Message buffer size */ +#define EXT4L_MSG_BUF_SIZE 4096 + /* Global state */ static struct blk_desc *ext4l_dev_desc; static struct disk_partition ext4l_part; @@ -31,6 +35,10 @@ static int ext4l_mounted; /* Global super_block pointer for filesystem operations */ static struct super_block *ext4l_sb; +/* Message recording buffer */ +static struct membuf ext4l_msg_buf; +static char ext4l_msg_data[EXT4L_MSG_BUF_SIZE]; + /** * ext4l_get_blk_dev() - Get the current block device * Return: Block device descriptor or NULL if not mounted @@ -91,6 +99,34 @@ void ext4l_clear_blk_dev(void) ext4l_mounted = 0; } +/** + * ext4l_msg_init() - Initialize the message buffer + */ +static void ext4l_msg_init(void) +{ + membuf_init(&ext4l_msg_buf, ext4l_msg_data, EXT4L_MSG_BUF_SIZE); +} + +/** + * ext4l_record_msg() - Record a message in the buffer + * @msg: Message string to record + * @len: Length of message + */ +void ext4l_record_msg(const char *msg, int len) +{ + membuf_put(&ext4l_msg_buf, msg, len); +} + +/** + * ext4l_get_msg_buf() - Get the message buffer + * + * Return: Pointer to the message buffer + */ +struct membuf *ext4l_get_msg_buf(void) +{ + return &ext4l_msg_buf; +} + int ext4l_probe(struct blk_desc *fs_dev_desc, struct disk_partition *fs_partition) { @@ -105,6 +141,9 @@ int ext4l_probe(struct blk_desc *fs_dev_desc, if (!fs_dev_desc) return -EINVAL; + /* Initialise message buffer for recording ext4 messages */ + ext4l_msg_init(); + /* Initialise CRC32C table for checksum verification */ ext4l_crc32c_init(); diff --git a/fs/ext4l/super.c b/fs/ext4l/super.c index 47e0b2c92e8..57653c8acb7 100644 --- a/fs/ext4l/super.c +++ b/fs/ext4l/super.c @@ -943,11 +943,9 @@ void __ext4_std_error(struct super_block *sb, const char *function, void __ext4_msg(struct super_block *sb, const char *prefix, const char *fmt, ...) { - struct va_format vaf; + char buf[256]; va_list args; - - if (!IS_ENABLED(CONFIG_EXT4L_DEBUG)) - return; + int len; if (sb) { atomic_inc(&EXT4_SB(sb)->s_msg_count); @@ -956,14 +954,28 @@ void __ext4_msg(struct super_block *sb, return; } + /* Format the message into a buffer */ va_start(args, fmt); - vaf.fmt = fmt; - vaf.va = &args; if (sb) - printk("%sEXT4-fs (%s): %pV\n", prefix, sb->s_id, &vaf); + len = snprintf(buf, sizeof(buf), "%sEXT4-fs (%s): ", + prefix, sb->s_id); else - printk("%sEXT4-fs: %pV\n", prefix, &vaf); + len = snprintf(buf, sizeof(buf), "%sEXT4-fs: ", prefix); + len += vsnprintf(buf + len, sizeof(buf) - len, fmt, args); va_end(args); + + /* Add newline if there's space */ + if (len < sizeof(buf) - 1) { + buf[len++] = '\n'; + buf[len] = '\0'; + } + + /* Record in message buffer */ + ext4l_record_msg(buf, len); + + /* Also print if debug is enabled */ + if (IS_ENABLED(CONFIG_EXT4L_DEBUG)) + printf("%s", buf); } static int ext4_warning_ratelimit(struct super_block *sb) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add runtime control for printing ext4 messages via the ext4l_msgs environment variable. When set to 'y' or '1', the recorded message buffer is printed after a successful mount. This provides a runtime alternative to the compile-time CONFIG_EXT4L_DEBUG option. Usage: setenv ext4l_msgs y Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/interface.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 9b6f4c0b02b..638f51d8c64 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -10,6 +10,7 @@ */ #include <blk.h> +#include <env.h> #include <membuf.h> #include <part.h> #include <malloc.h> @@ -127,6 +128,20 @@ struct membuf *ext4l_get_msg_buf(void) return &ext4l_msg_buf; } +/** + * ext4l_print_msgs() - Print all recorded messages + * + * Prints the contents of the message buffer to the console. + */ +static void ext4l_print_msgs(void) +{ + char *data; + int len; + + while ((len = membuf_getraw(&ext4l_msg_buf, 80, true, &data)) > 0) + printf("%.*s", len, data); +} + int ext4l_probe(struct blk_desc *fs_dev_desc, struct disk_partition *fs_partition) { @@ -263,6 +278,11 @@ int ext4l_probe(struct blk_desc *fs_dev_desc, /* Store super_block for later operations */ ext4l_sb = sb; + + /* Print messages if ext4l_msgs environment variable is set */ + if (env_get_yesno("ext4l_msgs") == 1) + ext4l_print_msgs(); + return 0; err_free_buf: -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add initial tests for the ext4l filesystem driver: - fs_test_ext4l_probe_norun: verifies the driver can probe and mount an ext4 filesystem - fs_test_ext4l_msgs_norun: verifies the ext4l_msgs env var causes mount messages to be printed The C tests use UTF_MANUAL flag and accept fs_image argument, following the pattern established by fs_basic.c. The Python wrapper creates an ext4 image and calls the C tests. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/fs/Makefile | 1 + test/fs/ext4l.c | 81 +++++++++++++++++++++++++++++ test/py/tests/test_fs/test_ext4l.py | 59 +++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 test/fs/ext4l.c create mode 100644 test/py/tests/test_fs/test_ext4l.py diff --git a/test/fs/Makefile b/test/fs/Makefile index 5899be8e667..a8fd1227a1d 100644 --- a/test/fs/Makefile +++ b/test/fs/Makefile @@ -1,3 +1,4 @@ # SPDX-License-Identifier: GPL-2.0+ obj-y += fs_basic.o +obj-$(CONFIG_FS_EXT4L) += ext4l.o diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c new file mode 100644 index 00000000000..e566c9e97b0 --- /dev/null +++ b/test/fs/ext4l.c @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Tests for ext4l filesystem (Linux ext4 port) + * + * Copyright 2025 Canonical Ltd + * Written by Simon Glass <simon.glass@canonical.com> + */ + +#include <command.h> +#include <env.h> +#include <ext4l.h> +#include <fs.h> +#include <fs_legacy.h> +#include <u-boot/uuid.h> +#include <test/test.h> +#include <test/ut.h> +#include <test/fs.h> + +#define EXT4L_ARG_IMAGE 0 /* fs_image: path to filesystem image */ + +/** + * fs_test_ext4l_probe_norun() - Test probing an ext4l filesystem + * + * This test verifies that the ext4l driver can successfully probe and + * mount an ext4 filesystem image. + * + * Arguments: + * fs_image: Path to the ext4 filesystem image + */ +static int fs_test_ext4l_probe_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)); + ut_assertok(fs_set_blk_dev("host", "0", FS_TYPE_ANY)); + + return 0; +} +FS_TEST_ARGS(fs_test_ext4l_probe_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL, + { "fs_image", UT_ARG_STR }); + +/** + * fs_test_ext4l_msgs_norun() - Test ext4l_msgs env var output + * + * This test verifies that setting ext4l_msgs=y causes mount messages + * to be printed when probing an ext4 filesystem. + * + * Arguments: + * fs_image: Path to the ext4 filesystem image + */ +static int fs_test_ext4l_msgs_norun(struct unit_test_state *uts) +{ + const char *fs_image = ut_str(EXT4L_ARG_IMAGE); + char uuid_str[UUID_STR_LEN + 1]; + u8 uuid[16]; + + ut_assertnonnull(fs_image); + ut_assertok(env_set("ext4l_msgs", "y")); + console_record_reset_enable(); + ut_assertok(run_commandf("host bind 0 %s", fs_image)); + ut_assertok(fs_set_blk_dev("host", "0", FS_TYPE_ANY)); + + /* Get the UUID and clear the env var now we have the output */ + ut_assertok(ext4l_get_uuid(uuid)); + uuid_bin_to_str(uuid, uuid_str, UUID_STR_FORMAT_STD); + ut_assertok(env_set("ext4l_msgs", NULL)); + + /* + * Check messages. The probe test runs first and doesn't unmount, + * so the journal needs recovery. Verify both messages. + */ + ut_assert_nextline("EXT4-fs (ext4l_mmc0): recovery complete"); + ut_assert_nextline("EXT4-fs (ext4l_mmc0): mounted filesystem %s r/w with ordered data mode. Quota mode: disabled.", + uuid_str); + ut_assert_console_end(); + + return 0; +} +FS_TEST_ARGS(fs_test_ext4l_msgs_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 new file mode 100644 index 00000000000..3b206293cbc --- /dev/null +++ b/test/py/tests/test_fs/test_ext4l.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright 2025 Canonical Ltd +# Written by Simon Glass <simon.glass@canonical.com> +# +# Test for ext4l filesystem driver + +""" +Test ext4l filesystem probing via C unit test. +""" + +import os +from subprocess import CalledProcessError, check_call + +import pytest + + +@pytest.mark.boardspec('sandbox') +class TestExt4l: + """Test ext4l filesystem operations.""" + + @pytest.fixture(scope='class') + def ext4_image(self, u_boot_config): + """Create an ext4 filesystem image for testing. + + Args: + u_boot_config (u_boot_config): U-Boot configuration. + + Yields: + str: Path to the ext4 image file. + """ + image_path = os.path.join(u_boot_config.persistent_data_dir, + 'ext4l_test.img') + try: + # Create a 64MB ext4 image + 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) + except CalledProcessError: + pytest.skip('Failed to create ext4 image') + + yield image_path + + # Cleanup + if os.path.exists(image_path): + os.remove(image_path) + + def test_probe(self, ubman, ext4_image): + """Test that ext4l can probe an ext4 filesystem.""" + with ubman.log.section('Test ext4l probe'): + output = ubman.run_command( + f'ut -f fs fs_test_ext4l_probe_norun fs_image={ext4_image}') + assert 'failures: 0' in output + + def test_msgs(self, ubman, ext4_image): + """Test that ext4l_msgs env var produces mount messages.""" + with ubman.log.section('Test ext4l msgs'): + output = ubman.run_command( + f'ut -f fs fs_test_ext4l_msgs_norun fs_image={ext4_image}') + assert 'failures: 0' in output -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Remove the static qualifier from ext4_readdir() so it can be called from the ext4l interface code for directory listing operations. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/dir.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ext4l/dir.c b/fs/ext4l/dir.c index 9c4b853defa..c527847a300 100644 --- a/fs/ext4l/dir.c +++ b/fs/ext4l/dir.c @@ -122,7 +122,7 @@ int __ext4_check_dir_entry(const char *function, unsigned int line, return 1; } -static int ext4_readdir(struct file *file, struct dir_context *ctx) +int ext4_readdir(struct file *file, struct dir_context *ctx) { unsigned int offset; int i; -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The dir_emit() macro was stubbed to do nothing, which meant directory listing would never output any entries. Replace it with a proper inline function that calls ctx->actor() to emit each directory entry. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 116f0475217..52f479e9673 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -1703,8 +1703,12 @@ extern struct inode *iget_locked(struct super_block *sb, unsigned long ino); #define inode_eq_iversion(i, v) ({ (void)(i); (void)(v); 1; }) #define inode_query_iversion(i) ({ (void)(i); 0ULL; }) -/* Directory context operations */ -#define dir_emit(ctx, name, len, ino, type) ({ (void)(ctx); (void)(name); (void)(len); (void)(ino); (void)(type); 1; }) +/* Directory context operations - call the actor callback */ +static inline bool dir_emit(struct dir_context *ctx, const char *name, int len, + u64 ino, unsigned int type) +{ + return ctx->actor(ctx, name, len, ctx->pos, ino, type) == 0; +} #define dir_relax_shared(i) ({ (void)(i); 1; }) /* File llseek */ -- 2.43.0
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
participants (1)
-
Simon Glass