From patchwork Fri Apr 3 14:04:43 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 2106 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=1775225190; bh=774jexmY42H8raaHYc2ryRw52f74bHJN6xS8SFHQ1hE=; 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=hGbGHzqGpUmkBnYbWyrkrG1rkaIZnj6gjjtOxLp6GUbprv+j2tWo0tJDL/oOwr8ME RRnX+1UzGNXGhQRXG2OI6o6gPm+FaHNPam+SsjmV5/QVdEpnkMj1iZCDC1wRwzkl8B 4dkNvUHa5NYlNK8XOh6k+Znbbn2wX1Gdxbovgm4ywA/yNsZWP9wTJ8TEPK7Mxx+yt9 UXraqxJ8LwCAJMV5i1y/FlEi21J3aotwSqQtaAGlm5E9liCfF7RffCLqLU0dGLLg2U gljBXfkoaD99H7n/Uno5984QdFvIrA9bGdhV/3UsvL5ojRuiPZJ+aw+6aFlK13deYO /BnbsLviHUNEg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 9A9196A378 for ; Fri, 3 Apr 2026 08:06:30 -0600 (MDT) 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 XMaXqdlK9YoV for ; Fri, 3 Apr 2026 08:06:30 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775225188; bh=774jexmY42H8raaHYc2ryRw52f74bHJN6xS8SFHQ1hE=; 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=kGzpjj3OLmGViMY9M5tk0KUY1n4jQU5o2JqSUIW+x7aHmt++hMDBxpV2Tu/P91BWn CPF8MUswzIIIVhn2M8bej4fXf2qFnYBc9Q536wlZxZOjuKQKtDT/7IufSxQddDPKDh lLZTWhXiL9Ftbi7JNkUuEhBHuHZuApbEKpLvisw4EWPsRMElxtaNUxgr5q1hJc3o5u cJn8HMLpNhrf4qAVkE6vNMLckZhbM6w/zBjj4mdpFuB9cRrat5jfSuOAYVbQ9ao7m/ 0L0z2AYiG0Ol2C5tBml29CVevCmSc0VQGLr9kQ4Mbe+2hbuYT6lhuZ5gxHRTynfd47 MAzPBLot7annw== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 9A8296A34F for ; Fri, 3 Apr 2026 08:06:28 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775225185; bh=04mP5Arj7JcFpnTqAWGL9N8drvHUOsI6fKxLcRsc6W4=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=eTsneRamS95aexeYrn+HzlpTOwmet/Yo3BZxRENtg8T5YpqucOnLEZIm4hX1DTb5D pMrABuoZXAYwEZxbY4XnqBCT/XUhI4Corm1JLdzxqCQBy+wutg5pn7p3yvnw7Vf7rF g2Jtm/V4Prb4xU1ux22FFK/Wya6F+2xaXa5yDnQi+8WrWDoYZp6gYJlwn9/Vr4Nhz4 FF+r/N5PUYRyKdMRbnVQIbEEC+H/0jXBaLW3RGqHRXupugPHqF2JKtzxKuf2ZcZj6p F6ik/+rj5NlBIMvIqQhEe7Nd+a25p9elm/WfM+OKfKMuWN4ksM3IR071oLadsmw/3r SZWObvuZBvPFg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id C4F0E67EE6; Fri, 3 Apr 2026 08:06:25 -0600 (MDT) 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 ZyR-lokDSndm; Fri, 3 Apr 2026 08:06:25 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775225183; bh=doqX/nUOgV4P1s34Y3Esa9C7Z3J86FYAmnUKaA+wdTM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=RYlzhFY41h9BKjQdKzVuDNQ58QaowH79TmJmhfzPh38lO9idxWaZUs2m2p8AqArVj 7Va/2hH4dEEnc08EcY22gRXe/iPA9WJ4Hu2W0zKaecw6wibQKVFoc6tVp2QhNYxx7/ 1UD2qkWs8BdNzmfdO/ZOLOWL5aZ5We5RoFcFJGMURdyY+VYUxY3LkgQb2jRZPFsCzG fAgGbAAIYGiavqyGuu7wW2VwzGAczWsWCUo8aLPmRHAr0vvUiYYoh1Gc/b1X04RrhH kcy75EGsACblfcFS5ieAwvrkaSwijzizT3qa7vxtfQtUJ4tyQaQT6xSplH4SNT1feH R7Gtyeb1s+arQ== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 8C9696A35F; Fri, 3 Apr 2026 08:06:23 -0600 (MDT) From: Simon Glass To: U-Boot Concept Date: Fri, 3 Apr 2026 08:04:43 -0600 Message-ID: <20260403140523.1998228-22-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260403140523.1998228-1-sjg@u-boot.org> References: <20260403140523.1998228-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: HUYFAV2GGITM4PWD4CSVVAUYMAMCFKZ2 X-Message-ID-Hash: HUYFAV2GGITM4PWD4CSVVAUYMAMCFKZ2 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: Simon Glass X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 21/34] cmd: Add save, stat, cd, pwd and cp commands 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 Add VFS commands for save, stat, cd, pwd and cp (as an fs subcommand). These use the VFS layer for path resolution, current working directory and file access. Signed-off-by: Simon Glass --- cmd/fs.c | 175 ++++++++++++++++++++++++++++++++++++++++++++- cmd/vfs.c | 9 ++- include/vfs.h | 8 +++ test/dm/fs.c | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 6 deletions(-) diff --git a/cmd/fs.c b/cmd/fs.c index 944d15b4520..e3fad83d68c 100644 --- a/cmd/fs.c +++ b/cmd/fs.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 /* - * VFS-based filesystem commands - mount, umount, ls, load, size + * VFS-based filesystem commands - mount, umount, ls, load, save, size * * These replace the legacy commands in cmd/fs_legacy.c with versions that * use absolute paths through the virtual filesystem layer. @@ -213,6 +213,126 @@ U_BOOT_CMD_COMPLETE( vfs_cmd_complete ); +#define COPY_BUF_SIZE 0x1000 + +int do_cp(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]) +{ + struct file_uc_priv *uc_priv; + struct udevice *src, *dst; + char buf[COPY_BUF_SIZE]; + loff_t remaining, pos; + long total = 0; + int ret; + + if (argc < 3) + return CMD_RET_USAGE; + + ret = vfs_open_file(argv[1], DIR_O_RDONLY, &src); + if (ret) { + printf("Source '%s' not found: %dE\n", argv[1], ret); + return CMD_RET_FAILURE; + } + + ret = vfs_open_file(argv[2], DIR_O_WRONLY, &dst); + if (ret) { + printf("Dest '%s' failed: %dE\n", argv[2], ret); + return CMD_RET_FAILURE; + } + + uc_priv = dev_get_uclass_priv(src); + remaining = uc_priv->size; + pos = 0; + + while (remaining > 0) { + long chunk = min((loff_t)COPY_BUF_SIZE, remaining); + long nread, nwritten; + + nread = file_read_at(src, buf, pos, chunk); + if (nread < 0) { + printf("Read failed: %ldE\n", nread); + return CMD_RET_FAILURE; + } + if (!nread) + break; + + nwritten = file_write_at(dst, buf, pos, nread); + if (nwritten < 0) { + printf("Write failed: %ldE\n", nwritten); + return CMD_RET_FAILURE; + } + + pos += nread; + remaining -= nread; + total += nwritten; + } + + printf("%ld bytes copied\n", total); + + return CMD_RET_SUCCESS; +} + +static const char *fs_type_name(unsigned int type) +{ + switch (type) { + case FS_DT_DIR: + return "directory"; + case FS_DT_REG: + return "regular file"; + case FS_DT_LNK: + return "symbolic link"; + default: + return "unknown"; + } +} + +static int do_stat(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + struct fs_dirent dent; + int ret; + + if (argc < 2) + return CMD_RET_USAGE; + + ret = vfs_stat(argv[1], &dent); + if (ret) { + printf("Error: %dE\n", ret); + return CMD_RET_FAILURE; + } + + printf(" File: %s\n", dent.name); + printf(" Size: %llu\n", dent.size); + printf(" Type: %s\n", fs_type_name(dent.type)); + if (dent.change_time.tm_year) { + printf("Modify: %04d-%02d-%02d %02d:%02d:%02d\n", + dent.change_time.tm_year, dent.change_time.tm_mon, + dent.change_time.tm_mday, dent.change_time.tm_hour, + dent.change_time.tm_min, dent.change_time.tm_sec); + } + if (dent.access_time.tm_year) { + printf("Access: %04d-%02d-%02d %02d:%02d:%02d\n", + dent.access_time.tm_year, dent.access_time.tm_mon, + dent.access_time.tm_mday, dent.access_time.tm_hour, + dent.access_time.tm_min, dent.access_time.tm_sec); + } + if (dent.create_time.tm_year) { + printf(" Birth: %04d-%02d-%02d %02d:%02d:%02d\n", + dent.create_time.tm_year, dent.create_time.tm_mon, + dent.create_time.tm_mday, dent.create_time.tm_hour, + dent.create_time.tm_min, dent.create_time.tm_sec); + } + + return CMD_RET_SUCCESS; +} + +U_BOOT_CMD_COMPLETE( + stat, 2, 1, do_stat, + "display file status", + "\n" + " - Show type, size and timestamps of a file or directory", + vfs_cmd_complete +); + static int do_size(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]) { @@ -233,12 +353,13 @@ static int do_size(struct cmd_tbl *cmdtp, int flag, int argc, return CMD_RET_SUCCESS; } -U_BOOT_CMD( +U_BOOT_CMD_COMPLETE( size, 2, 0, do_size, "determine a file's size", "\n" " - Find file at 'path' in the VFS, determine its size,\n" - " and store in the 'filesize' variable." + " and store in the 'filesize' variable.", + vfs_cmd_complete ); static int do_vfs_load(struct cmd_tbl *cmdtp, int flag, int argc, @@ -330,3 +451,51 @@ U_BOOT_CMD_COMPLETE( " - Legacy: load from block device interface", vfs_cmd_complete ); + +static int do_save(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + struct udevice *fil; + long bytes, written; + unsigned long addr; + loff_t pos = 0; + void *buf; + int ret; + + if (argc < 4) + return CMD_RET_USAGE; + + addr = hextoul(argv[1], NULL); + bytes = hextoul(argv[3], NULL); + if (argc >= 5) + pos = hextoul(argv[4], NULL); + + ret = vfs_open_file(argv[2], DIR_O_WRONLY, &fil); + if (ret) { + printf("Error: %dE\n", ret); + return CMD_RET_FAILURE; + } + + buf = map_sysmem(addr, bytes); + written = file_write_at(fil, buf, pos, bytes); + unmap_sysmem(buf); + + if (written < 0) { + printf("Write failed: %ldE\n", written); + return CMD_RET_FAILURE; + } + + printf("%ld bytes written\n", written); + + return CMD_RET_SUCCESS; +} + +U_BOOT_CMD_COMPLETE( + save, 5, 0, do_save, + "save memory to a file", + " [pos]\n" + " - Save 'bytes' from address 'addr' to 'path' in the VFS.\n" + " 'pos' gives the file byte position to start writing to.\n" + " If 'pos' is 0 or omitted, the file is written from the start.", + vfs_cmd_complete +); diff --git a/cmd/vfs.c b/cmd/vfs.c index 5779f3f098d..33407da4594 100644 --- a/cmd/vfs.c +++ b/cmd/vfs.c @@ -104,11 +104,14 @@ static int do_fs_ls(struct cmd_tbl *cmdtp, int flag, int argc, U_BOOT_LONGHELP(fs, "mount [ ] - list or create mounts\n" - "fs mount -t - mount from block device\n" + "fs mount - auto-detect and mount\n" + "fs mount -t - mount specific type\n" "fs umount - unmount a filesystem\n" - "fs ls [] - list directory (default /)"); + "fs ls [] - list directory (default /)\n" + "fs cp - copy a file"); U_BOOT_CMD_WITH_SUBCMDS(fs, "Filesystem operations", fs_help_text, U_BOOT_SUBCMD_MKENT(mount, 6, 1, do_fs_mount), U_BOOT_SUBCMD_MKENT(umount, 2, 1, do_fs_umount), - U_BOOT_SUBCMD_MKENT(ls, 2, 1, do_fs_ls)); + U_BOOT_SUBCMD_MKENT(ls, 2, 1, do_fs_ls), + U_BOOT_SUBCMD_MKENT(cp, 3, 0, do_cp)); diff --git a/include/vfs.h b/include/vfs.h index 7c31ded7796..04cbd5d0ca9 100644 --- a/include/vfs.h +++ b/include/vfs.h @@ -292,6 +292,14 @@ int fs_mount_blkdev(const char *type, struct blk_desc *desc, int part_num, */ void vfs_print_mounts(void); +/** + * do_cp() - Copy a file within the VFS + * + * Implements the 'fs cp' subcommand. Defined in cmd/fs.c, declared here + * so that cmd/vfs.c can reference it. + */ +int do_cp(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]); + #ifdef CONFIG_AUTO_COMPLETE /** * vfs_complete() - Complete a partial VFS path diff --git a/test/dm/fs.c b/test/dm/fs.c index 0ef52ca0ee4..8bba5263546 100644 --- a/test/dm/fs.c +++ b/test/dm/fs.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #define READ_SIZE 0x20 @@ -232,6 +233,15 @@ static int dm_test_vfs_complete(struct unit_test_state *uts) /* Non-matching prefix should return 0 */ ut_asserteq(0, vfs_complete(buf, "/z", 16, cmdv)); + /* Create a known file to complete against */ + memcpy(map_sysmem(0x10000, 5), "hello", 5); + ut_assertok(run_command("save 10000 /host/.comp_test 5", 0)); + console_record_reset_enable(); + + /* Complete inside mount */ + ut_assert(vfs_complete(buf, "/host/.comp_", 16, cmdv) >= 1); + ut_asserteq_str(".comp_test", cmdv[0]); + /* Complete with empty prefix shows entries */ ut_assert(vfs_complete(buf, "/host/", 16, cmdv) >= 1); @@ -460,4 +470,186 @@ static int dm_test_vfs_cwd(struct unit_test_state *uts) } DM_TEST(dm_test_vfs_cwd, UTF_SCAN_FDT); +/* Test cd and pwd commands */ +static int dm_test_vfs_cd(struct unit_test_state *uts) +{ + ut_assertok(vfs_init()); + + ut_assertok(run_command("mount hostfs /host", 0)); + ut_assert_console_end(); + + /* Default cwd is root */ + ut_assertok(run_command("pwd", 0)); + ut_assert_nextline("/"); + ut_assert_console_end(); + + /* cd to an absolute path */ + ut_assertok(run_command("cd /host", 0)); + ut_assert_console_end(); + ut_assertok(run_command("pwd", 0)); + ut_assert_nextline("/host"); + ut_assert_console_end(); + + /* ls without a path should list cwd */ + ut_assertok(run_command("ls", 0)); + ut_assert_skip_to_linen("DIR "); + console_record_reset_enable(); + + /* cd back to root */ + ut_assertok(run_command("cd /", 0)); + ut_assert_console_end(); + ut_assertok(run_command("pwd", 0)); + ut_assert_nextline("/"); + ut_assert_console_end(); + + ut_assertok(run_command("umount /host", 0)); + ut_assert_console_end(); + + return 0; +} +DM_TEST(dm_test_vfs_cd, UTF_SCAN_FDT); + +/* Test multi-component path resolution (files in subdirectories) */ +static int dm_test_vfs_path(struct unit_test_state *uts) +{ + ut_assertok(vfs_init()); + + ut_assertok(run_command("mount hostfs /host", 0)); + ut_assert_console_end(); + + /* stat a file in a subdirectory */ + ut_assertok(run_command("stat /host/cmd/vfs.c", 0)); + ut_assert_nextline(" File: vfs.c"); + ut_assert_nextlinen(" Size: "); + ut_assert_nextline(" Type: regular file"); + console_record_reset_enable(); + + /* load from a subdirectory */ + ut_assertok(run_command("load 1000000 /host/cmd/vfs.c", 0)); + ut_assert_nextlinen("%s", ""); + ut_assert_console_end(); + + /* ls a nested subdirectory */ + ut_assertok(run_command("ls /host/arch", 0)); + ut_assert_skip_to_linen("DIR "); + console_record_reset_enable(); + + ut_assertok(run_command("umount /host", 0)); + ut_assert_console_end(); + + return 0; +} +DM_TEST(dm_test_vfs_path, UTF_SCAN_FDT); + +/* Test the cp command via VFS */ +static int dm_test_vfs_cp(struct unit_test_state *uts) +{ + char buf[32]; + + ut_assertok(vfs_init()); + ut_assertok(run_command("mount hostfs /host", 0)); + ut_assert_console_end(); + + /* Write a source file */ + memcpy(map_sysmem(0x1000, 5), "hello", 5); + ut_assertok(run_command("save 1000 /host/.cp_src 5", 0)); + ut_assert_nextline("5 bytes written"); + ut_assert_console_end(); + + /* Copy it */ + ut_assertok(run_command("fs cp /host/.cp_src /host/.cp_dst", 0)); + ut_assert_nextline("5 bytes copied"); + ut_assert_console_end(); + + /* Read back the copy and verify */ + memset(map_sysmem(0x2000, 8), 0, 8); + ut_assertok(run_command("load 2000 /host/.cp_dst", 0)); + ut_assert_nextline("5 bytes read"); + ut_assert_console_end(); + + memcpy(buf, map_sysmem(0x2000, 5), 5); + buf[5] = '\0'; + ut_asserteq_str("hello", buf); + + os_unlink(".cp_src"); + os_unlink(".cp_dst"); + + ut_assertok(run_command("umount /host", 0)); + ut_assert_console_end(); + + return 0; +} +DM_TEST(dm_test_vfs_cp, UTF_SCAN_FDT); + +/* Test the stat command via VFS */ +static int dm_test_vfs_stat(struct unit_test_state *uts) +{ + ut_assertok(vfs_init()); + + ut_assertok(run_command("mount hostfs /host", 0)); + ut_assert_console_end(); + + /* stat a regular file */ + ut_assertok(run_command("stat /host/README", 0)); + ut_assert_nextline(" File: README"); + ut_assert_nextlinen(" Size: "); + ut_assert_nextline(" Type: regular file"); + console_record_reset_enable(); + + /* stat a directory */ + ut_assertok(run_command("stat /host/cmd", 0)); + ut_assert_nextline(" File: cmd"); + ut_assert_nextlinen(" Size: "); + ut_assert_nextline(" Type: directory"); + console_record_reset_enable(); + + /* stat non-existent file */ + ut_asserteq(1, run_command("stat /host/does-not-exist", 0)); + console_record_reset_enable(); + + ut_assertok(run_command("umount /host", 0)); + ut_assert_console_end(); + + return 0; +} +DM_TEST(dm_test_vfs_stat, UTF_SCAN_FDT); + +/* Test save and load round-trip via VFS */ +static int dm_test_vfs_save(struct unit_test_state *uts) +{ + char buf[32]; + + ut_assertok(vfs_init()); + + /* Mount hostfs */ + ut_assertok(run_command("mount hostfs /host", 0)); + ut_assert_console_end(); + + /* Write a known pattern to a temp file */ + memset(map_sysmem(0x1000, 16), 0, 16); + memcpy(map_sysmem(0x1000, 11), "hello world", 11); + ut_assertok(run_command("save 1000 /host/.vfs_test_tmp 0xb", 0)); + ut_assert_nextline("11 bytes written"); + ut_assert_console_end(); + + /* Read it back and verify */ + memset(map_sysmem(0x2000, 16), 0, 16); + ut_assertok(run_command("load 2000 /host/.vfs_test_tmp", 0)); + ut_assert_nextline("11 bytes read"); + ut_assert_console_end(); + + memcpy(buf, map_sysmem(0x2000, 11), 11); + buf[11] = '\0'; + ut_asserteq_str("hello world", buf); + + /* Clean up the temp file */ + os_unlink(".vfs_test_tmp"); + + ut_assertok(run_command("umount /host", 0)); + ut_assert_console_end(); + + return 0; +} +DM_TEST(dm_test_vfs_save, UTF_SCAN_FDT); + #endif