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:
1 parent
b5038f8673
commit
c187b7bce3
12 files changed
+1741
No files matched your search
@@ -0,0 +1,280 @@
|
||||
/* logic/fsx.c — 文件管理器业务逻辑实现(纯 C:仅 libc + 标准头,无 meow_ui 依赖)。
|
||||
*
|
||||
* 实现要点:
|
||||
* - 所有写操作(delete/rename/mkdir)先 fsx_within_sd() 复核目标路径,拒绝 ".." 越界与卡外路径;
|
||||
* - 路径一律经 fsx_join() 由「已验证 base + sanitize 后的 name」构造,杜绝手工拼串引入穿越;
|
||||
* - 目录枚举用 bounded 数组(FSX_MAX_ENTRIES)+ 客户端虚拟化只画可见行——不一次全读进内存;
|
||||
* - 符号链接 v1 不追(is_dir=0,作普通文件展示),防止链出卡外。
|
||||
*/
|
||||
#include "fsx.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ---- 路径安全 ---- */
|
||||
|
||||
int fsx_within_sd(const char *path)
|
||||
{
|
||||
if (!path) return 0;
|
||||
size_t rl = strlen(FSX_SD_ROOT);
|
||||
if (strncmp(path, FSX_SD_ROOT, rl) != 0) return 0;
|
||||
char c = path[rl];
|
||||
if (c != '\0' && c != '/') return 0; /* "…/sd_0evil" 这类前缀伪装拒绝 */
|
||||
|
||||
/* 逐 '/'-分隔分量扫描:出现 ".." 即拒绝(防 "../../" 逃逸)。 */
|
||||
const char *p = path;
|
||||
while (*p) {
|
||||
while (*p == '/') p++;
|
||||
if (!*p) break;
|
||||
const char *start = p;
|
||||
while (*p && *p != '/') p++;
|
||||
size_t len = (size_t)(p - start);
|
||||
if (len == 2 && start[0] == '.' && start[1] == '.') return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int fsx_is_root(const char *path)
|
||||
{
|
||||
return path && strcmp(path, FSX_SD_ROOT) == 0;
|
||||
}
|
||||
|
||||
int fsx_dirname(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!path || !out || outsz <= 0) return -1;
|
||||
if (fsx_is_root(path)) {
|
||||
size_t rl = strlen(FSX_SD_ROOT);
|
||||
if ((int)rl >= outsz) return -1;
|
||||
memcpy(out, FSX_SD_ROOT, rl + 1);
|
||||
return 0;
|
||||
}
|
||||
const char *slash = strrchr(path, '/');
|
||||
if (!slash) return -1; /* 非法相对路径 */
|
||||
size_t n = (size_t)(slash - path);
|
||||
if (n == 0) n = 1; /* "/xxx" → "/"(一般不出现) */
|
||||
if ((int)(n + 1) > outsz) return -1;
|
||||
memcpy(out, path, n);
|
||||
out[n] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *fsx_basename(const char *path)
|
||||
{
|
||||
if (!path) return "";
|
||||
const char *slash = strrchr(path, '/');
|
||||
return slash ? slash + 1 : path;
|
||||
}
|
||||
|
||||
int fsx_join(const char *base, const char *name, char *out, int outsz)
|
||||
{
|
||||
if (!base || !name || !out || outsz <= 0) return -1;
|
||||
/* name 必须是被清洗后的单分量:无 '/'、非 ".."、非空。 */
|
||||
if (!name[0] || name[0] == '/' || strcmp(name, "..") == 0 || strchr(name, '/')) return -1;
|
||||
|
||||
size_t bl = strlen(base);
|
||||
/* base 尾随 '/' 去掉,但保持根 "/usr/data/mnt/sd_0" 不截成 ""。 */
|
||||
char b[FSX_MAX_PATH];
|
||||
if (bl >= sizeof b) return -1;
|
||||
memcpy(b, base, bl + 1);
|
||||
while (bl > 0 && b[bl - 1] == '/' && bl != strlen(FSX_SD_ROOT)) b[--bl] = '\0';
|
||||
|
||||
int n = snprintf(out, (size_t)outsz, "%s/%s", b, name);
|
||||
if (n < 0 || n >= outsz) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fsx_sanitize_name(const char *name, char *out, int outsz)
|
||||
{
|
||||
if (!name || !out || outsz <= 0) return -1;
|
||||
const char *s = name;
|
||||
/* 跳过前导空白 */
|
||||
while (*s == ' ' || *s == '\t') s++;
|
||||
if (!*s) return -1; /* 全空白 */
|
||||
if (strlen(s) > 120) return -1; /* 过长 */
|
||||
if (s[0] == '.') return -1; /* 隐藏/点位起始拒 */
|
||||
for (const char *p = s; *p; p++) {
|
||||
unsigned char ch = (unsigned char)*p;
|
||||
if (ch < 0x20) return -1; /* 控制字符 */
|
||||
if (*p == '/' || *p == '\\' || *p == ':') return -1;
|
||||
if (p[0] == '.' && p[1] == '.') return -1; /* ".." 分量拒 */
|
||||
}
|
||||
size_t n = strlen(s);
|
||||
/* 去尾随空白 */
|
||||
while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t')) n--;
|
||||
if (n == 0) return -1;
|
||||
if ((int)n + 1 > outsz) return -1;
|
||||
memcpy(out, s, n);
|
||||
out[n] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 展示格式化 ---- */
|
||||
|
||||
void fsx_fmt_size(long long size, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
if (size < 0) size = 0;
|
||||
const char *u[] = { "B", "KB", "MB", "GB" };
|
||||
double v = (double)size;
|
||||
int i = 0;
|
||||
while (v >= 1024.0 && i < 3) { v /= 1024.0; i++; }
|
||||
if (i == 0) snprintf(out, (size_t)outsz, "%lld B", size);
|
||||
else snprintf(out, (size_t)outsz, "%.1f %s", v, u[i]);
|
||||
}
|
||||
|
||||
void fsx_fmt_time(long long mtime, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
time_t t = (time_t)mtime;
|
||||
struct tm tm;
|
||||
if (!localtime_r(&t, &tm)) { snprintf(out, (size_t)outsz, "-"); return; }
|
||||
strftime(out, (size_t)outsz, "%Y-%m-%d %H:%M", &tm);
|
||||
}
|
||||
|
||||
/* ---- 目录枚举 / 排序 / 虚拟化 ---- */
|
||||
|
||||
static int entry_cmp(const void *a, const void *b)
|
||||
{
|
||||
const fsx_entry *ea = (const fsx_entry *)a;
|
||||
const fsx_entry *eb = (const fsx_entry *)b;
|
||||
if (ea->is_dir != eb->is_dir) return eb->is_dir - ea->is_dir; /* 目录优先 */
|
||||
return strcmp(ea->name, eb->name); /* 名称升序 */
|
||||
}
|
||||
|
||||
void fsx_sort_entries(fsx_entry *entries, int n)
|
||||
{
|
||||
if (n > 1) qsort(entries, (size_t)n, sizeof *entries, entry_cmp);
|
||||
}
|
||||
|
||||
int fsx_scan_dir(const char *path, fsx_entry *entries, int max, int *out_count)
|
||||
{
|
||||
if (out_count) *out_count = 0;
|
||||
if (!path) return FSX_ERR_NOENT;
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -errno; /* ENOENT(-2)/EACCES(-13)/ENOTDIR(-20) */
|
||||
|
||||
int n = 0;
|
||||
struct dirent *de;
|
||||
char jbuf[FSX_MAX_PATH];
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.') continue; /* 跳过 . .. 及隐藏项 */
|
||||
if (n >= max) break; /* 超大目录截断(客户端标注) */
|
||||
fsx_entry *e = &entries[n];
|
||||
memset(e, 0, sizeof *e);
|
||||
if (fsx_join(path, de->d_name, jbuf, sizeof jbuf) != 0) continue;
|
||||
strncpy(e->name, de->d_name, FSX_MAX_NAME - 1);
|
||||
e->name[FSX_MAX_NAME - 1] = '\0';
|
||||
strncpy(e->path, jbuf, FSX_MAX_PATH - 1);
|
||||
e->path[FSX_MAX_PATH - 1] = '\0';
|
||||
|
||||
struct stat st;
|
||||
if (lstat(jbuf, &st) == 0) {
|
||||
e->is_link = S_ISLNK(st.st_mode);
|
||||
if (S_ISDIR(st.st_mode)) { e->is_dir = 1; }
|
||||
else { e->is_dir = 0; e->size = st.st_size; }
|
||||
e->mtime = st.st_mtime;
|
||||
} else {
|
||||
e->is_dir = 0; e->size = 0; e->mtime = 0;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
fsx_sort_entries(entries, n);
|
||||
if (out_count) *out_count = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fsx_max_scroll(int count, int item_h, int viewport_h)
|
||||
{
|
||||
int m = count * item_h - viewport_h;
|
||||
return m > 0 ? m : 0;
|
||||
}
|
||||
|
||||
void fsx_visible_range(int count, int item_h, int scroll, int viewport_h,
|
||||
int *first, int *last)
|
||||
{
|
||||
if (first) *first = 0;
|
||||
if (last) *last = 0;
|
||||
if (count <= 0 || item_h <= 0 || viewport_h <= 0) return;
|
||||
if (scroll < 0) scroll = 0;
|
||||
int f = scroll / item_h;
|
||||
if (f < 0) f = 0;
|
||||
if (f > count) f = count;
|
||||
int l = (scroll + viewport_h) / item_h + 1;
|
||||
if (l > count) l = count;
|
||||
if (first) *first = f;
|
||||
if (last) *last = l;
|
||||
}
|
||||
|
||||
/* ---- 写/查操作 ---- */
|
||||
|
||||
int fsx_delete(const char *path)
|
||||
{
|
||||
if (!path || !fsx_within_sd(path)) return -EACCES;
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -errno;
|
||||
if (S_ISDIR(st.st_mode)) { return rmdir(path) == 0 ? 0 : -errno; }
|
||||
return unlink(path) == 0 ? 0 : -errno;
|
||||
}
|
||||
|
||||
int fsx_rename_into(const char *path, const char *new_name)
|
||||
{
|
||||
if (!path || !fsx_within_sd(path)) return -EACCES;
|
||||
char nb[FSX_MAX_NAME], db[FSX_MAX_PATH], np[FSX_MAX_PATH];
|
||||
if (fsx_sanitize_name(new_name, nb, sizeof nb) != 0) return -EINVAL;
|
||||
if (fsx_dirname(path, db, sizeof db) != 0) return -EINVAL;
|
||||
if (fsx_join(db, nb, np, sizeof np) != 0) return -EINVAL;
|
||||
if (!fsx_within_sd(np)) return -EACCES;
|
||||
struct stat st;
|
||||
if (lstat(np, &st) == 0) return -EEXIST; /* 目标已存在,拒绝覆盖 */
|
||||
return rename(path, np) == 0 ? 0 : -errno;
|
||||
}
|
||||
|
||||
int fsx_mkdir(const char *base, const char *name)
|
||||
{
|
||||
if (!base || !fsx_within_sd(base)) return -EACCES;
|
||||
char nb[FSX_MAX_NAME], np[FSX_MAX_PATH];
|
||||
if (fsx_sanitize_name(name, nb, sizeof nb) != 0) return -EINVAL;
|
||||
if (fsx_join(base, nb, np, sizeof np) != 0) return -EINVAL;
|
||||
if (!fsx_within_sd(np)) return -EACCES;
|
||||
return mkdir(np, 0777) == 0 ? 0 : -errno;
|
||||
}
|
||||
|
||||
int fsx_stat_path(const char *path, fsx_entry *out)
|
||||
{
|
||||
if (!path || !out) return -EINVAL;
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -errno;
|
||||
memset(out, 0, sizeof *out);
|
||||
strncpy(out->name, fsx_basename(path), FSX_MAX_NAME - 1);
|
||||
out->name[FSX_MAX_NAME - 1] = '\0';
|
||||
strncpy(out->path, path, FSX_MAX_PATH - 1);
|
||||
out->path[FSX_MAX_PATH - 1] = '\0';
|
||||
out->is_link = S_ISLNK(st.st_mode);
|
||||
out->is_dir = S_ISDIR(st.st_mode);
|
||||
out->size = st.st_size;
|
||||
out->mtime = st.st_mtime;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fsx_is_text_path(const char *path)
|
||||
{
|
||||
if (!path) return 0;
|
||||
const char *dot = strrchr(path, '.');
|
||||
if (!dot) return 0;
|
||||
static const char *exts[] = {
|
||||
".txt", ".log", ".md", ".json", ".ini", ".cfg", ".conf", ".csv", ".html",
|
||||
".xml", ".srt", ".lrc", ".sh", ".c", ".h", ".py", ".js", ".ts", ".css",
|
||||
".nfo", ".m3u", ".m3u8", ".list", ".license", ".readme", ".gitignore", NULL
|
||||
};
|
||||
for (int i = 0; exts[i]; i++)
|
||||
if (strcasecmp(dot, exts[i]) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/* logic/fsx.h — 文件管理器业务逻辑接口(纯 C,不依赖 meow_ui.h / meow_view)。
|
||||
*
|
||||
* 分层契约(沿用 hello_app/logic/logic.h 的约定):
|
||||
* - UI→业务 = 纯函数命令 + 查询(fsx_*);业务不回调 UI,UI 按返回值/err 自绘。
|
||||
* - 本模块只含 libc + 标准头,因此可宿主编译(gcc 直接测),也可交叉编译进 MIPS 应用。
|
||||
*
|
||||
* 宿主可验证的纯逻辑(test/test_fsx.c 驱动):
|
||||
* - 路径安全:fsx_within_sd / fsx_join / fsx_sanitize_name / fsx_is_root / fsx_basename
|
||||
* - 展示格式化:fsx_fmt_size / fsx_fmt_time
|
||||
* - 列表数据源:fsx_visible_range(虚拟化可见行)/ fsx_sort_entries(目录优先)
|
||||
* - 目录枚举/写操作:fsx_scan_dir / fsx_delete / fsx_rename_into / fsx_mkdir / fsx_stat_path
|
||||
* (这些做真 IO,仍可宿主测:指向临时目录)
|
||||
*/
|
||||
#ifndef FSX_H
|
||||
#define FSX_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* TF 卡根(挂载点)。写操作只允许在它之下。
|
||||
* 允许编译期 -DFSX_SD_ROOT=... 覆写(宿主测试指向临时目录,便于测写操作)。 */
|
||||
#ifndef FSX_SD_ROOT
|
||||
#define FSX_SD_ROOT "/usr/data/mnt/sd_0"
|
||||
#endif
|
||||
|
||||
#define FSX_MAX_NAME 256 /* 文件名/路径分量上限(exFAT 255 字节)×... 保守 */
|
||||
#define FSX_MAX_PATH 512 /* 一条完整路径上限 */
|
||||
#define FSX_MAX_ENTRIES 1024/* 单目录缓存项上限(大目录截断 + 列表虚拟化只画可见行) */
|
||||
|
||||
/* 返回码(等价负 errno;0=成功)。与 errno 数值一致,便于 App 判空态/权限态。 */
|
||||
#define FSX_ERR_IO (-1)
|
||||
#define FSX_ERR_NOENT (-2) /* 路径不存在(卡不在/目录被移除) */
|
||||
#define FSX_ERR_EACCES (-13) /* 权限拒绝 */
|
||||
#define FSX_ERR_ENOTDIR (-20) /* 不是目录 */
|
||||
|
||||
/* 单个目录项快照(App 持有数组;fsx_scan_dir 填充)。 */
|
||||
typedef struct fsx_entry {
|
||||
char name[FSX_MAX_NAME]; /* 文件名(不含路径) */
|
||||
char path[FSX_MAX_PATH]; /* 绝对路径(在卡内,fsx_join 构造) */
|
||||
int is_dir; /* 1=目录 */
|
||||
int is_link; /* 1=符号链接(v1 视为文件,不追链) */
|
||||
long long size; /* 字节(目录=0) */
|
||||
long long mtime; /* 修改时间(Unix 秒) */
|
||||
} fsx_entry;
|
||||
|
||||
/* ============================================================ */
|
||||
/* 路径安全(纯逻辑,写操作红线) */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 1=path 严格落在 FSX_SD_ROOT 之下(或等于根),且不含任何 ".." 分量。
|
||||
* 拒绝 "/usr/data/mnt/sd_0evil" 这类前缀伪装,也拒绝 "../" 越界。 */
|
||||
int fsx_within_sd(const char *path);
|
||||
|
||||
/* 1=path 精确等于 FSX_SD_ROOT。 */
|
||||
int fsx_is_root(const char *path);
|
||||
|
||||
/* 安全拼接:base + "/" + name → out。name 须无 '/'、非 ".."、非空(通常已 sanitize)。
|
||||
* 返回 0=成功;-1=参数非法/输出超长。 */
|
||||
int fsx_join(const char *base, const char *name, char *out, int outsz);
|
||||
|
||||
/* 校验+清洗新建/重命名名(供 mkdir/rename):拒绝空/超长/含 '/'、'\\'、':'、以 '.' 开头、
|
||||
* 含 ".."、含控制字符。清洗后拷入 out 并去尾随空白。返回 0=合法;负数=非法(-EINVAL)。 */
|
||||
int fsx_sanitize_name(const char *name, char *out, int outsz);
|
||||
|
||||
/* 返回 path 最后一个 '/' 之后的分量(借用指针,不改写)。 */
|
||||
const char *fsx_basename(const char *path);
|
||||
|
||||
/* 拷贝 path 的目录部分(去掉最后分量)到 out;根目录返回 FSX_SD_ROOT。返回 0=成功。 */
|
||||
int fsx_dirname(const char *path, char *out, int outsz);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 展示格式化(纯逻辑) */
|
||||
/* ============================================================ */
|
||||
|
||||
/* "123 B" / "1.2 KB" / "3.4 MB" / "1.2 GB"。 */
|
||||
void fsx_fmt_size(long long size, char *out, int outsz);
|
||||
|
||||
/* "YYYY-MM-DD HH:MM"(本地时区)。 */
|
||||
void fsx_fmt_time(long long mtime, char *out, int outsz);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 目录枚举 / 排序 / 虚拟化(列表数据源) */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 扫 path 目录到 entries[0..max),目录优先 + 名称升序。*out_count=读到的项数。
|
||||
* 超过 max 截断(大目录:只缓存前 max 项,配合虚拟化渲染)。
|
||||
* 返回 0=成功;<0=负 errno(-2=无卡/不存在;-13=权限)。空目录=0 且 count=0。 */
|
||||
int fsx_scan_dir(const char *path, fsx_entry *entries, int max, int *out_count);
|
||||
|
||||
/* 目录优先,其次 name 升序(strcmp,字节序)。 */
|
||||
void fsx_sort_entries(fsx_entry *entries, int n);
|
||||
|
||||
/* 虚拟化:给定总行数/行高/滚动/视口高,算可见 [first,last)。越界安全。 */
|
||||
void fsx_visible_range(int count, int item_h, int scroll, int viewport_h,
|
||||
int *first, int *last);
|
||||
|
||||
/* 最大滚动偏移:max(0, count*item_h - viewport_h)。 */
|
||||
int fsx_max_scroll(int count, int item_h, int viewport_h);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 写/查操作(全部先 fsx_within_sd 校验;0=成功,<0=负 errno) */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 删除:目录(须空)rmdir,否则 unlink。 */
|
||||
int fsx_delete(const char *path);
|
||||
|
||||
/* 重命名 *path 为 *new_name(同名目录下)。目标已存在→-EEXIST。 */
|
||||
int fsx_rename_into(const char *path, const char *new_name);
|
||||
|
||||
/* 在 base 目录下新建子目录 name。 */
|
||||
int fsx_mkdir(const char *base, const char *name);
|
||||
|
||||
/* stat 一个路径填 entry(lstat,不追符号链)。0=成功,<0=负 errno。 */
|
||||
int fsx_stat_path(const char *path, fsx_entry *out);
|
||||
|
||||
/* 简单文本判定:按扩展名白名单(用于读预览)。1=视为文本。 */
|
||||
int fsx_is_text_path(const char *path);
|
||||
|
||||
#endif /* FSX_H */
|
||||
Reference in new issue
Block a user