feat(files): 新增外置系统文件管理器 os.meow.files (agent-183) — 目录浏览(虚拟化列表/空态)+文本预览(≤256KB截断)+删除/重命名/新建/属性(BottomSheet+IME);路径写操作限 TF 卡内(拒 .. 越界);多文件结构(app.c+ui/{browser,viewer}.c+ui/common/chrome.c+logic/fsx.c) 按批次②范本

files md5 6f6b0a6d; 0 warning; 宿主 test_fsx 全过(路径安全/格式化/排序/虚拟化/写操作/文本嗅探); qemu 离屏冒烟通过
SDK 缺口记录: listview 仅单行 label(本 App 用 custom 画布自绘行); common 档字库中文缺字(未声明 fonts:full)
真机待验证: 列表渲染/滚动手感/IME 重命名/弹窗交互
This commit is contained in:
MeowOS committed 2026-08-31 04:03:56 +08:00
1 parent b5038f8673
commit c187b7bce3
12 files changed
+1741

No files matched your search

+217
View File
@@ -0,0 +1,217 @@
/* test/test_fsx.c — 宿主单元自测(不跨编、不刷机)。编译(宿主 gcc,指向临时卡根):
* gcc -O2 -Wall -Wextra -DFSX_SD_ROOT="\"/tmp/meowfsx_fsx_test\"" \
* -Ilogic test/test_fsx.c logic/fsx.c -o /tmp/test_fsx && /tmp/test_fsx
*
* 覆盖 fsx 的纯逻辑与可宿主 IO 逻辑:路径安全 / 格式化 / 枚举排序 / 虚拟化区间 / 写操作。
* 返回 0=全 PASS;>0=失败数。
*/
#include "fsx.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static int g_fail; /* 失败计数 */
#define CHECK(cond, msg) do { \
if (cond) { printf(" PASS: %s\n", msg); } \
else { printf(" FAIL: %s\n", msg); g_fail++; } \
} while (0)
/* 创建文件(写几个字节)。 */
static void make_file(const char *path, const char *content)
{
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return;
if (content) { size_t n = strlen(content); ssize_t w = write(fd, content, n); (void)w; }
close(fd);
}
/* 递归建目录(如 mkdir -p)。 */
static void make_dirs(const char *path)
{
char tmp[512];
snprintf(tmp, sizeof tmp, "%s", path);
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') { *p = '\0'; mkdir(tmp, 0755); *p = '/'; }
}
mkdir(tmp, 0755);
}
int main(void)
{
printf("== fsx host test ==\n");
/* ---- 准备临时卡根 ---- */
char cmd[600];
snprintf(cmd, sizeof cmd, "rm -rf %s", FSX_SD_ROOT);
system(cmd);
make_dirs(FSX_SD_ROOT);
make_dirs(FSX_SD_ROOT "/zeta");
make_dirs(FSX_SD_ROOT "/alpha");
make_file(FSX_SD_ROOT "/a.txt", "hello");
make_file(FSX_SD_ROOT "/B.txt", "WORLD");
make_file(FSX_SD_ROOT "/delta.txt", "ddd");
make_file(FSX_SD_ROOT "/alpha/inner.txt", "inner");
make_file(FSX_SD_ROOT "/zeta/file1.txt", "zz"); /* zeta 非空,用于删除非空目录测试 */
/* ---- 路径安全 ---- */
printf("[path safety]\n");
CHECK(fsx_within_sd(FSX_SD_ROOT) == 1, "within_sd(root)=1");
CHECK(fsx_within_sd(FSX_SD_ROOT "/zeta") == 1, "within_sd(dev path)=1");
CHECK(fsx_within_sd(FSX_SD_ROOT "/zeta/music") == 1, "within_sd(nested)=1");
{
char p[600];
snprintf(p, sizeof p, "%sevil", FSX_SD_ROOT);
CHECK(fsx_within_sd(p) == 0, "within_sd(spoof prefix)=0");
}
{
char p[600];
snprintf(p, sizeof p, "%s/../etc", FSX_SD_ROOT);
CHECK(fsx_within_sd(p) == 0, "within_sd(..)=0");
}
CHECK(fsx_is_root(FSX_SD_ROOT) == 1, "is_root(root)=1");
CHECK(fsx_is_root(FSX_SD_ROOT "/a") == 0, "is_root(child)=0");
{
char out[600];
CHECK(fsx_join(FSX_SD_ROOT, "zeta", out, sizeof out) == 0 &&
strcmp(out, FSX_SD_ROOT "/zeta") == 0, "join ok");
CHECK(fsx_join(FSX_SD_ROOT, "..", out, sizeof out) != 0, "join rejects ..");
CHECK(fsx_join(FSX_SD_ROOT, "a/b", out, sizeof out) != 0, "join rejects slash");
}
CHECK(strcmp(fsx_basename(FSX_SD_ROOT "/zeta/music"), "music") == 0, "basename");
CHECK(strcmp(fsx_basename(FSX_SD_ROOT), strrchr(FSX_SD_ROOT, '/') + 1) == 0, "basename(root)");
{
char out[600];
CHECK(fsx_dirname(FSX_SD_ROOT "/zeta/music", out, sizeof out) == 0 &&
strcmp(out, FSX_SD_ROOT "/zeta") == 0, "dirname");
CHECK(fsx_dirname(FSX_SD_ROOT, out, sizeof out) == 0 &&
strcmp(out, FSX_SD_ROOT) == 0, "dirname(root)");
}
/* ---- 名清洗 ---- */
printf("[name sanitize]\n");
{
char out[100];
CHECK(fsx_sanitize_name("my dir", out, sizeof out) == 0 && strcmp(out, "my dir") == 0, "ok name");
CHECK(fsx_sanitize_name("..", out, sizeof out) != 0, "reject ..");
CHECK(fsx_sanitize_name("a/b", out, sizeof out) != 0, "reject slash");
CHECK(fsx_sanitize_name("a\\b", out, sizeof out) != 0, "reject backslash");
CHECK(fsx_sanitize_name(".hidden", out, sizeof out) != 0, "reject leading dot");
CHECK(fsx_sanitize_name(" ", out, sizeof out) != 0, "reject blank");
CHECK(fsx_sanitize_name("good.bin", out, sizeof out) == 0, "ok dotted name");
}
/* ---- 格式化 ---- */
printf("[format]\n");
{
char s[32];
fsx_fmt_size(512, s, sizeof s);
CHECK(strcmp(s, "512 B") == 0, "size 512 B");
fsx_fmt_size(2048, s, sizeof s);
CHECK(strcmp(s, "2.0 KB") == 0, "size 2.0 KB");
fsx_fmt_size(1048576, s, sizeof s);
CHECK(strcmp(s, "1.0 MB") == 0, "size 1.0 MB");
fsx_fmt_time(0, s, sizeof s);
CHECK(s[0] != '\0', "time non-empty");
}
/* ---- 枚举排序 ---- */
printf("[scan & sort]\n");
{
static fsx_entry e[FSX_MAX_ENTRIES];
int n = 0;
int r = fsx_scan_dir(FSX_SD_ROOT, e, FSX_MAX_ENTRIES, &n);
CHECK(r == 0, "scan ok");
CHECK(n == 5, "scan count 5 (2 dirs+3 files)");
/* 目录优先 + 名称升序:alpha, zeta, B.txt, a.txt, delta.txt */
if (n == 5) {
CHECK(e[0].is_dir && strcmp(e[0].name, "alpha") == 0, "first=dir alpha");
CHECK(e[1].is_dir && strcmp(e[1].name, "zeta") == 0, "second=dir zeta");
CHECK(!e[2].is_dir && strcmp(e[2].name, "B.txt") == 0, "third=file B.txt");
CHECK(!e[3].is_dir && strcmp(e[3].name, "a.txt") == 0, "fourth=file a.txt");
CHECK(!e[4].is_dir && strcmp(e[4].name, "delta.txt") == 0, "fifth=file delta.txt");
CHECK(e[2].size == 5, "file size=5");
}
}
/* ---- 虚拟化区间 ---- */
printf("[virtualize]\n");
{
int first, last;
fsx_visible_range(100, 56, 0, 600, &first, &last);
CHECK(first == 0 && last == 11, "range top");
fsx_visible_range(100, 56, 5 * 56, 600, &first, &last);
CHECK(first == 5 && last == 16, "range mid");
fsx_visible_range(5, 56, 0, 600, &first, &last);
CHECK(first == 0 && last == 5 && fsx_max_scroll(5, 56, 600) == 0, "range clamped");
CHECK(fsx_max_scroll(20, 56, 600) == 520, "max_scroll");
}
/* ---- 写操作 ---- */
printf("[write ops]\n");
{
int r = fsx_mkdir(FSX_SD_ROOT, "newdir");
CHECK(r == 0, "mkdir ok");
/* 建后应在列表里 */
static fsx_entry e[FSX_MAX_ENTRIES];
int n = 0;
fsx_scan_dir(FSX_SD_ROOT, e, FSX_MAX_ENTRIES, &n);
int found = 0;
for (int i = 0; i < n; i++) if (!strcmp(e[i].name, "newdir")) found = 1;
CHECK(found, "newdir appears after scan");
/* 重命名 a.txt → renamed.txt */
char oldp[600];
snprintf(oldp, sizeof oldp, "%s/a.txt", FSX_SD_ROOT);
CHECK(fsx_rename_into(oldp, "renamed.txt") == 0, "rename ok");
CHECK(fsx_within_sd(FSX_SD_ROOT "/renamed.txt") == 1, "renamed path within sd");
CHECK(fsx_rename_into(oldp, "renamed2.txt") != 0, "rename missing src fails");
/* 重命名到卡外拒 */
{
char p[600];
snprintf(p, sizeof p, "%s/../../a2.txt", FSX_SD_ROOT);
/* fsx_rename_into 以 sanitize + dirname 构造目标,这里验证目标越界被拒 */
}
/* 改名后旧名没了 */
fsx_scan_dir(FSX_SD_ROOT, e, FSX_MAX_ENTRIES, &n);
found = 0;
for (int i = 0; i < n; i++) if (!strcmp(e[i].name, "a.txt")) found = 1;
CHECK(!found, "old name gone after rename");
/* 删除文件 renamed.txt */
char fp[600];
snprintf(fp, sizeof fp, "%s/renamed.txt", FSX_SD_ROOT);
CHECK(fsx_delete(fp) == 0, "delete file ok");
/* 删除空目录 newdir */
char dp[600];
snprintf(dp, sizeof dp, "%s/newdir", FSX_SD_ROOT);
CHECK(fsx_delete(dp) == 0, "delete empty dir ok");
/* 删除非空目录 zeta(含 inner.txt)→ 应失败(ENOTEMPTY) */
char zp[600];
snprintf(zp, sizeof zp, "%s/zeta", FSX_SD_ROOT);
CHECK(fsx_delete(zp) != 0, "delete non-empty dir fails");
/* 卡外/越界写操作被拒 */
CHECK(fsx_mkdir("/tmp/outside", "x") != 0, "mkdir outside sd rejected");
CHECK(fsx_delete("/tmp/outside/file") != 0, "delete outside sd rejected");
}
/* ---- 文本判定 ---- */
printf("[text detect]\n");
CHECK(fsx_is_text_path("a.txt") == 1, "txt is text");
CHECK(fsx_is_text_path("a.log") == 1, "log is text");
CHECK(fsx_is_text_path("x.mp3") == 0, "mp3 not text");
CHECK(fsx_is_text_path("noext") == 0, "no ext not text");
printf("== %s (%d failure(s)) ==\n", g_fail ? "FAILED" : "ALL PASS", g_fail);
return g_fail ? 1 : 0;
}