/* * 模块名称:假 FatFs 内存卷(存储域主机侧单测共享件) * 模块功能:给 App/log 与 App/cfg 的主机侧单测一个可脚本化的假文件系统—— * 类型与常量取自**真实 Lib/FatFs/ff.h**(FRESULT / FIL / FILINFO / FA_* * 一字不改),本件在 fatfs_fake.c 里给出九个 f_* 原型的 RAM 实现: * f_open / f_read / f_write / f_lseek / f_close / f_stat / f_unlink / * f_mkdir / f_rename。 * * 仅供 test/host/** 使用:不参与 MCU 构建,也不参与 simulator 构建。 * * 为什么共享一份(照 test/host/common/ch395f_fake.{h,c} 的先例): * sys_log_mcu / soe_store / cfg_file_store_mcu 三块的成败**全部**由 * 「这次 f_open 拿到什么码、f_write 写回多少字节」决定。三份假件各写一遍, * 漂移 = 「假件与被测代码同时错而无人报错」。签名与真实头同源 → 真实 API 一改 * 就编译期报错(同 test/host/nand_port 的「真头 + 假 diskio」路子)。 * * —— 必须复刻的三条本仓实证语义(docs/架构设计/STM32F4-Base存储架构说明.md §10.1)—— * 1) FA_OPEN_APPEND(0x30) **不含 FA_WRITE(0x02)**:本移植 FA_OPEN_APPEND = * FA_OPEN_ALWAYS(0x10) | FA_SEEKEND(0x20),而 FA_SEEKEND 只定义在 ff.c:74、 * ff.h 里没有。所以只给 FA_OPEN_APPEND 打开的文件**能打开、能创建**,但 * f_write 恒返 FR_DENIED(7)——2026-09-20 台架「/log 写失败 x5/开机」的真根因 * (§10.1 第 11 条)。假件按真实 ff.c 一样把 mode 原值存进 fp->flag 再判 * flag & FA_WRITE,不做任何「便利放行」。 * 2) f_write 簇分配失败 = **FR_OK + 短写**(无错误码,判失败只能校验写回字节数): * 真实 ff.c:4102 的 if (clst == 0) break; → 循环带着已写计数退出 → FR_OK。 * 假件把「每文件固定后备簇池」当介质:写到装不下就返回 FR_OK + 截断字节数; * 另给定点注入,让「恰好短一字节」这种真机要撞坏块才有的形态可复现。 * 3) FF_USE_LFN=0 → **严格 8.3 短名**:主名 <=8、扩展 <=3、非法字符集 * * + ; < = > [ ] | 与 0x7F(ff.c create_name 非 LFN 分支逐字模拟)。 * 「.fschk」的真实死法 = 开头那个点把游标直接推进扩展域、随后 5 个字符撑爆 * 扩展域上限 3 → FR_INVALID_NAME(6);路径里任何位置出现 ':' 被 get_ldnumber * 当盘符前缀吃下 → FR_INVALID_DRIVE(11)。两者都是**纯 RAM 里的名字判定、 * 不碰盘**(§10.1 第 10 条,含「测试用例须同时接受这两个码」的口径)。 * * —— 其余与真实 ff.c 对齐的路径/错误码细则 —— * - 前导分隔符被剥掉、恒从卷根开始(FF_FS_RPATH=0,见 ff.c follow_path); * - 中间目录段不存在 → FR_NO_PATH;末段不存在且未给创建位 → FR_NO_FILE; * - 对目录本身 f_open(无创建位)→ FR_NO_FILE; * - FA_CREATE_NEW 撞上已存在 → FR_EXIST;FA_CREATE_ALWAYS → 截零 + 刷新时间戳; * - 路径剥完分隔符为空 → FR_INVALID_NAME(ff.c create_name 的 i==0 分支); * - f_stat:父目录在而对象不在 → FR_NO_FILE;父目录也不在 → FR_NO_PATH; * - f_unlink 不存在 → FR_NO_FILE;f_mkdir 已存在 → FR_EXIST; * - **f_rename 撞已存在的新名 → FR_EXIST、不覆盖**(ff.c「new name already in * use」,2026-09-22 台架暴露的那条:缺了它,一切「目标存在的覆盖写」 * 必然失败,而模拟器端 POSIX rename 的覆盖语义照不出来)。不复刻本条 = * cfg_store_write_atomic 里「先 unlink 再 rename」那一步永远测不出必要性; * - f_lseek 越过文件尾:只读句柄**钳到 objsize 且不报错**(ff.c 正常寻址分支), * 可写句柄才扩展文件; * - f_open 失败即作废句柄(ff.c「fp->obj.fs = 0」)→ 后续 f_write 拿 * FR_INVALID_OBJECT,不会拿着脏句柄继续写; * - f_close 成功即释放句柄,并把打开期间的变更落成新的 fdate/ftime * (真实 ff.c 在 close 里用 get_fattime 回写目录项)。 * * —— 脚本化模型(沿用本仓既有假件风格:nand_port 的 RAM 假芯片 / lftpd_io 的假堆)—— * - 注入:按路径 + op 预置「接下来 N 次返回某码 / 只写回 n 字节 / 只读回 n 字节」; * path 传 NULL = 不限路径; * - 观测:逐 op 调用计数 + 最近一次入参(路径、mode、btw/bw、btr/br、lseek 偏移), * 让「这轮压根不该碰盘」与「打开模式必须带写位」成为可断言的事; * - 卷:条目直读直写(ff_fake_mkfile / ff_fake_mkfile_size / ff_fake_fetch)用来 * 铺前置状态——「run.log 已经有 1024 条记录」不可能靠真写 65536 字节铺出来; * - 假时钟:ff_fake_set_dostime 控制 fdate/ftime,FAT 分钟粒度导致 * 「同分钟内同长度重写不可检出」这件事只有它能测(cfg_store_stat 的 mtime 令牌); * - ff_fake_reset() 一次全清(卷 + 注入 + 观测 + 时钟)。 * * 有意**不提供** f_mount / f_mkfs / f_opendir / f_readdir:三个被测模块一次都不该 * 调它们,真调了就是链接期报错——比运行时断言更硬,也不会静默通过。 * * 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2) */ #ifndef FATFS_FAKE_H #define FATFS_FAKE_H #include /* 真实头在 Lib/FatFs/;本文件不重定义任何 FatFs 类型或宏 */ #include "ff.h" #define FF_FAKE_MAX_ENT 16u /* 卷内条目上限(文件 + 目录) */ #define FF_FAKE_PATH_MAX 24u /* 归一化后路径上限("/log/selftest.log" = 17) */ /* 每文件的后备簇池:必须**大于生产侧单文件容量上限**(run.log / soe.log * = 1024 条 x 64 B = 65536 B,见 sys_log.h:SYSLOG_RUN_MAX 与 soe_store.h: * SOE_STORE_MAX),否则测"容量满 unlink 重建"时追加位落在池外,f_write 恒 * 返回 0 字节短写,轮转场景根本跑不到。写满仍然如实返回 FR_OK + 短写 *(真实 ff.c 簇分配失败的同一形态)。16 条目 x 66 KB ≈ 1 MB 静态,主机侧无压力。 */ #define FF_FAKE_CLUSTER 67584u #define FF_FAKE_INJ_MAX 8u /* 注入槽位数 */ #define FF_FAKE_GUARD 50000 /* 跑飞保险:单个 f_* 调用数超此值直接 exit(2) */ /* 注入与计数的操作编号(fatfs_fake.c 内部同一套) */ enum { FF_FAKE_OP_OPEN = 0, FF_FAKE_OP_READ, FF_FAKE_OP_WRITE, FF_FAKE_OP_LSEEK, FF_FAKE_OP_CLOSE, FF_FAKE_OP_STAT, FF_FAKE_OP_UNLINK, FF_FAKE_OP_MKDIR, FF_FAKE_OP_RENAME, FF_FAKE_OP_N }; /* 注入动作 */ enum { FF_FAKE_ACT_RC = 0, /* 直接返回指定 FRESULT,不做任何数据搬运 */ FF_FAKE_ACT_WSHORT, /* 仅 f_write:FR_OK 但只写回 n 字节 */ FF_FAKE_ACT_RSHORT /* 仅 f_read :FR_OK 但只读回 n 字节 */ }; typedef struct { int calls[FF_FAKE_OP_N]; /* 逐 op 调用次数(含失败的调用) */ BYTE last_open_mode; /* 最近一次 f_open 的 mode 原值(断言 FA_WRITE 位) */ char last_open_path[FF_FAKE_PATH_MAX]; char last_unlink_path[FF_FAKE_PATH_MAX]; char last_mkdir_path[FF_FAKE_PATH_MAX]; char last_rename_old[FF_FAKE_PATH_MAX]; char last_rename_new[FF_FAKE_PATH_MAX]; UINT last_write_btw; /* 请求写的字节数 */ UINT last_write_bw; /* 实际写回的字节数(短写的断言面) */ UINT last_read_btr; UINT last_read_br; FSIZE_t last_lseek_ofs; int last_open_rc; /* 最近一次 f_open 的返回码 */ int last_write_rc; /* 最近一次 f_write 的返回码 */ } ff_fake_obs_t; extern ff_fake_obs_t g_ff_obs; /* —— 世界控制 —— */ void ff_fake_reset(void); /* 卷 + 注入 + 观测 + 时钟一次全清 */ void ff_fake_set_dostime(uint16_t fdate, uint16_t ftime); /* 假 get_fattime() 的两个域 */ /* —— 故障注入(times = 生效次数;path = NULL 表示不限路径)—— */ void ff_fake_inject_rc(const char *path, int op, FRESULT rc, int times); void ff_fake_inject_write_short(const char *path, UINT n, int times); void ff_fake_inject_read_short(const char *path, UINT n, int times); /* —— 卷内容直读直写(绕开打开语义,用来铺前置状态)—— */ int ff_fake_exists(const char *path); /* 0 无 / 1 文件 / 2 目录 */ uint32_t ff_fake_size(const char *path); /* 无此文件返回 0xFFFFFFFF */ uint32_t ff_fake_time_token(const char *path); /* (fdate<<16)|ftime,同 cfg_store_stat */ uint32_t ff_fake_fetch(const char *path, uint8_t *p_buf, uint32_t off, uint32_t cap); int ff_fake_mkfile(const char *path, const uint8_t *p_data, uint32_t len); int ff_fake_mkfile_size(const char *path, uint32_t size); /* 只给逻辑长度(造容量满) */ int ff_fake_mkdir_direct(const char *path); /* 无条件建目录 */ int ff_fake_count_entries(void); /* 卷内条目数(含目录) */ #endif /* FATFS_FAKE_H */