794906cdc8
P2 五个文件全部进分母(三名队友并行,写范围互斥、零越界):
net_socket 420 条 / 881-883=99.8%、52-52 函数(S1~S6 分片)
sys_log_mcu 75 条 / 310-313=99.0%、12-12
console_log 104 条 / 188-189=99.5%、15-15
cfg_file_store_mcu 34 条 / 97-97=100%、10-10
soe_store 26 条 / 78-79=98.7%、4-4
合计 659 条断言,P2 本体 1554-1561=99.6%、93-93 函数。
账:套件 28→33 全绿;files_tested 32→37;未触及 112→107(.c 34→29,
恒等式 29 = 0(P1剩) + 0(P2) + 29(P3) 咬合,§9 终点判据达成);分母
4684→6245 插桩行;line 85.8%→89.3%;func 90.1%→92.2%(这批 5 个文件
全都以 ≥98.7% 进分母,故未摊薄)。--update-baseline 前后逐文件比对:
下降 0 个文件。
基建:新建 test/host/common/fatfs_fake.{h,c}(以真实 ff.h 为声明源、只
定义三文件实际调用的 9 个 f_*,缺哪个链接期响;卷忙/短写/FR_DENIED
可注入;短名违规分档);扩展 ch395f_fake 为纯新增(g_fx/g_fxchip 另开
一份世界 + ext_reset 挂进既有复位,既有三套面一字不动),并按真头修正
两个错值常量(SOCKET_OPEN 0x01→0x05、TCP_ESTABLISHED 0x05→0x04),
另在 net_socket runner 加真头/假件同名常量文本核对闸(负例验证过)。
存量回归:net_listener 34、net_events 34、net_pending 43、net_select 22、
nand_port 149、lftpd_io 33、vol_lock 33、soe 32、cfg_file 151 全绿。
编号:补 TC-HOST-{NETSOCK,SYSLOG,CFGFST,SOESTORE,CONLOG} 五族(22→27,
先补族再引号);net_socket 模块码由自取的两字母 NS 正名为 NETSOCK;
id-check 增量拦截无台账外新账,可判定比例 83.27%(基线 82.6%)。
文档:覆盖率口径 §9 新增「P2 落地」(含两套分母口径差、10 条现状表、
12 条可复用手法与并发红线),§8 第 1 条改「清单已吃穿」,补变更记录。
本提交未改任何生产代码(App/Drivers/Src/Lib/MDK-ARM 零触碰)。
10 条现状已逐条登记在 §9(含 sys_log_mcu.c:509~537 卷忙整批丢日志、
net_socket.c 同文件两套 IP 字节序、UDP 两 API 绕过 netTask),已获指示
全部修复,另开一轮(含测试字面量统一纯 ASCII)。
gate: pre-commit run --all-files → 39 hooks, 38 Passed; only mem-numbers
Failed(本工作树从未 @build,无 .map 事实源,属分支期已知陷阱);
mem-budget Passed;test-coverage 与 id-check Passed。RAM/Flash 增量 = 0
(纯测试批次)。按仓内分支约定走 --no-verify,台账回填留待主树合并后。
1078 lines
30 KiB
C
1078 lines
30 KiB
C
/*
|
||
* 模块名称:假 FatFs 内存卷(实现)
|
||
* 模块功能:实现 fatfs_fake.h 声明的九个 f_* 原型与卷控制/注入/观测接口。
|
||
* 由测试单元 #include 本 .c 使用(照 test/host/common/ch395f_fake.c 的做法)。
|
||
*
|
||
* 语义与 Lib/FatFs/ff.c 的对应关系全部写在 fatfs_fake.h 文件头;本文件只在
|
||
* 每条"复刻点"上标一句 ff.c 的出处,不再重复论证。
|
||
*
|
||
* 刻意不做的事:
|
||
* - 不做真 FAT 扇区/簇链模拟——被测的是应用侧对 FRESULT 与写回字节数的处置,
|
||
* 不是 ff.c 自己(ff.c 属 Lib/ 第三方,不进覆盖率分母);
|
||
* - 不做线程安全:三个被测模块都是"唯一写者 + 卷锁串行化",假卷跟着单线程即可;
|
||
* - 不实现 f_mount/f_mkfs/f_readdir:调用即链接失败(见头文件说明)。
|
||
*
|
||
* 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2)
|
||
*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
#include "fatfs_fake.h"
|
||
|
||
/* ff.c 内部标志(ff.h 不导出 FA_SEEKEND,本移植 FA_OPEN_APPEND = 0x10|0x20) */
|
||
#define FF_FAKE_SEEKEND 0x20u
|
||
#define FF_FAKE_CREATE_MASK (FA_CREATE_ALWAYS | FA_OPEN_ALWAYS | FA_CREATE_NEW)
|
||
|
||
/* f_open 成功时写进 fp->obj.fs 的"已挂载卷"指针(真实 ff.c 存的就是 FATFS*,
|
||
* 失败清 0)。用本文件里一个真对象的地址而不是硬造整数:MSVC x64 下
|
||
* (FATFS *)0xF01DBEEE 会刷 C4312「转换到大得多的类型」。 */
|
||
static FATFS s_vol;
|
||
#define FF_FAKE_FS_MAGIC (&s_vol)
|
||
|
||
typedef struct {
|
||
int used;
|
||
int is_dir;
|
||
uint32_t size; /* 逻辑长度(可大于已存内容) */
|
||
uint32_t stored; /* data[] 里真实存了多少字节 */
|
||
uint16_t fdate;
|
||
uint16_t ftime;
|
||
char path[FF_FAKE_PATH_MAX]; /* 归一化:全大写、以 '/' 开头、无重复分隔符 */
|
||
uint8_t data[FF_FAKE_CLUSTER];
|
||
} ff_fake_ent_t;
|
||
|
||
typedef struct {
|
||
int used;
|
||
int any_path; /* 1 = 不限路径 */
|
||
int op;
|
||
int act;
|
||
int skip; /* >0:本次命中先放行(skip--),不注入 */
|
||
FRESULT rc;
|
||
UINT n;
|
||
int times;
|
||
char path[FF_FAKE_PATH_MAX];
|
||
} ff_fake_inj_t;
|
||
|
||
ff_fake_obs_t g_ff_obs;
|
||
|
||
static ff_fake_ent_t s_ent[FF_FAKE_MAX_ENT];
|
||
static ff_fake_inj_t s_inj[FF_FAKE_INJ_MAX];
|
||
static uint16_t s_dostime_d = 0x5A21u; /* 2025-10-06 的 FAT 日期编码(任意基准) */
|
||
static uint16_t s_dostime_t = 0x6E40u; /* 13:52:00 */
|
||
static uint8_t s_dirty[FF_FAKE_MAX_ENT];
|
||
|
||
/* ─────────────────────────── 小工具 ─────────────────────────── */
|
||
|
||
static void s_guard(int op)
|
||
{
|
||
g_ff_obs.calls[op]++;
|
||
if (g_ff_obs.calls[op] > FF_FAKE_GUARD) {
|
||
(void)fprintf(stderr, "[FATAL] fatfs_fake: op %d call guard tripped\n", op);
|
||
exit(2);
|
||
}
|
||
}
|
||
|
||
static char s_up(char c)
|
||
{
|
||
if ((c >= 'a') && (c <= 'z')) {
|
||
return (char)(c - 32);
|
||
}
|
||
return c;
|
||
}
|
||
|
||
static int s_ieq(const char *a, const char *b)
|
||
{
|
||
while ((*a != '\0') && (*b != '\0')) {
|
||
if (s_up(*a) != s_up(*b)) {
|
||
return 0;
|
||
}
|
||
a++;
|
||
b++;
|
||
}
|
||
return (*a == *b) ? 1 : 0;
|
||
}
|
||
|
||
static int s_is_sep(char c)
|
||
{
|
||
return ((c == '/') || (c == '\\')) ? 1 : 0;
|
||
}
|
||
|
||
/* 真实 ff.c create_name 非 LFN 分支的逐字模拟:主名 <=8、扩展 <=3、非法字符集。
|
||
* 返回 1 = 合法短名段。 */
|
||
/* 非法字符单独判,不用字符串表:ff.c 的表里带双引号与反斜杠,
|
||
* 写进主机侧字符串字面量既易错又踩本仓"字面量必须纯 ASCII 且不含双引号"的坑 */
|
||
static int s_sfn_bad(char c)
|
||
{
|
||
switch (c) {
|
||
case '*': case '+': case ',': case ';':
|
||
case '<': case '=': case '>':
|
||
case '[': case ']': case '|':
|
||
case '"': case '?': case 0x7F:
|
||
return 1;
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
static int s_sfn_ok(const char *seg, int len)
|
||
{
|
||
int i = 0;
|
||
int ni = 8;
|
||
int k;
|
||
|
||
if (len <= 0) {
|
||
return 0; /* 真实 ff.c:i == 0 -> FR_INVALID_NAME */
|
||
}
|
||
for (k = 0; k < len; k++) {
|
||
char c = seg[k];
|
||
|
||
if ((c == '.') || (i >= ni)) { /* 主名结束 / 字段溢出? */
|
||
if (ni == 11) {
|
||
return 0; /* 扩展域已开:再来点或再溢出 = 非法(.fschk 死在这) */
|
||
}
|
||
if (c != '.') {
|
||
return 0; /* 主名溢出 */
|
||
}
|
||
i = 8;
|
||
ni = 11; /* 进入扩展域 */
|
||
continue;
|
||
}
|
||
if (s_sfn_bad(c) != 0) {
|
||
return 0; /* 非法字符(ff.c create_name 的 strchr 表) */
|
||
}
|
||
if (s_is_sep(c)) {
|
||
return 0; /* 段里不该有分隔符(调用方已切段) */
|
||
}
|
||
i++;
|
||
}
|
||
return (i == 0) ? 0 : 1;
|
||
}
|
||
|
||
/* 归一化路径:剥盘符 + 剥前导分隔符 + 全大写 + 折叠重复分隔符 + 去尾部分隔符。
|
||
* 返回 FRESULT;out 形如 "/LOG/RUN.LOG",根为 "/"。
|
||
* 盘符判定照 ff.c get_ldnumber:本移植 FF_VOLUMES=1,只有 "0:" 合法,
|
||
* 其它 "N:" 形态(含路径中间的冒号)一律 FR_INVALID_DRIVE。 */
|
||
static FRESULT s_norm(const char *path, char *out, int out_sz)
|
||
{
|
||
int o = 0;
|
||
int i = 0;
|
||
const char *p = path;
|
||
|
||
if (path == NULL) {
|
||
return FR_INVALID_NAME;
|
||
}
|
||
/* 盘符前缀:到第一个 ':' 之前的整体必须是单个数字 0 */
|
||
{
|
||
const char *q = p;
|
||
while ((*q != '\0') && (*q != ':')) {
|
||
q++;
|
||
}
|
||
if (*q == ':') {
|
||
if ((q == p + 1) && (p[0] == '0')) {
|
||
p = q + 1;
|
||
} else {
|
||
return FR_INVALID_DRIVE;
|
||
}
|
||
}
|
||
}
|
||
out[0] = '/';
|
||
o = 1;
|
||
while (s_is_sep(*p)) {
|
||
p++;
|
||
}
|
||
while (*p != '\0') {
|
||
const char *seg;
|
||
int len;
|
||
|
||
while (s_is_sep(*p)) {
|
||
p++;
|
||
}
|
||
if (*p == '\0') {
|
||
break; /* 尾部多余分隔符:折叠掉 */
|
||
}
|
||
seg = p;
|
||
len = 0;
|
||
while ((p[len] != '\0') && (!s_is_sep(p[len]))) {
|
||
len++;
|
||
}
|
||
if (!s_sfn_ok(seg, len)) {
|
||
return FR_INVALID_NAME;
|
||
}
|
||
if ((o + len + 1) >= out_sz) {
|
||
return FR_INVALID_NAME; /* 路径过长:假件一律以名字类错误拒绝 */
|
||
}
|
||
if (i != 0) {
|
||
out[o++] = '/';
|
||
}
|
||
{
|
||
int k;
|
||
for (k = 0; k < len; k++) {
|
||
out[o++] = s_up(seg[k]);
|
||
}
|
||
}
|
||
out[o] = '\0';
|
||
i++;
|
||
p += len;
|
||
}
|
||
if (o == 1) {
|
||
return FR_INVALID_NAME; /* 空路径 = 卷根本身:真实 f_open 报 NS_NONAME */
|
||
}
|
||
return FR_OK;
|
||
}
|
||
|
||
/* 取父目录路径:"/LOG/RUN.LOG" -> "/LOG","/LOG" -> "/" */
|
||
static void s_parent(const char *norm, char *out)
|
||
{
|
||
const char *slash = strrchr(norm, '/');
|
||
|
||
if ((slash == NULL) || (slash == norm)) {
|
||
out[0] = '/';
|
||
out[1] = '\0';
|
||
} else {
|
||
int n = (int)(slash - norm);
|
||
(void)memcpy(out, norm, (size_t)n);
|
||
out[n] = '\0';
|
||
}
|
||
}
|
||
|
||
static int s_find(const char *norm)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) {
|
||
if ((s_ent[i].used != 0) && (strcmp(s_ent[i].path, norm) == 0)) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
static int s_alloc(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) {
|
||
if (s_ent[i].used == 0) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
static int s_has_children(const char *dir_norm)
|
||
{
|
||
int i;
|
||
int dl = (int)strlen(dir_norm);
|
||
|
||
for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) {
|
||
if (s_ent[i].used == 0) {
|
||
continue;
|
||
}
|
||
if (((int)strlen(s_ent[i].path) > dl) &&
|
||
(strncmp(s_ent[i].path, dir_norm, (size_t)dl) == 0) &&
|
||
(s_ent[i].path[dl] == '/')) {
|
||
return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 中间目录段必须存在(真实 follow_path:非末段找不到 -> FR_NO_PATH) */
|
||
static FRESULT s_check_parent(const char *norm)
|
||
{
|
||
char par[FF_FAKE_PATH_MAX];
|
||
int idx;
|
||
|
||
s_parent(norm, par);
|
||
if (strcmp(par, "/") == 0) {
|
||
return FR_OK;
|
||
}
|
||
idx = s_find(par);
|
||
if (idx < 0) {
|
||
return FR_NO_PATH;
|
||
}
|
||
if (s_ent[idx].is_dir == 0) {
|
||
return FR_NO_PATH; /* 真实 follow_path:不是子目录 -> FR_NO_PATH */
|
||
}
|
||
return FR_OK;
|
||
}
|
||
|
||
/* 命中注入则返回 1 并把 rc/n 带出 */
|
||
static int s_hit(int op, const char *norm, FRESULT *rc, int *act, UINT *n)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; i < (int)FF_FAKE_INJ_MAX; i++) {
|
||
if (s_inj[i].used == 0) {
|
||
continue;
|
||
}
|
||
if (s_inj[i].op != op) {
|
||
continue;
|
||
}
|
||
if ((s_inj[i].any_path == 0) && (!s_ieq(s_inj[i].path, norm))) {
|
||
continue;
|
||
}
|
||
if (s_inj[i].skip > 0) {
|
||
/* 路径匹配上了才算"放行一次"——不然别的路径/别的 op 会把 skip 吃掉,
|
||
* 注入永远打不到真正要打击的那一次调用 */
|
||
s_inj[i].skip--;
|
||
return 0;
|
||
}
|
||
*rc = s_inj[i].rc;
|
||
*act = s_inj[i].act;
|
||
*n = s_inj[i].n;
|
||
s_inj[i].times--;
|
||
if (s_inj[i].times <= 0) {
|
||
s_inj[i].used = 0;
|
||
}
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 定长缓冲的字符串拷贝(不用 strncpy/strcpy:MSVC /W4 报 C4996 弃用,噪音) */
|
||
static void s_keep(char *dst, const char *src)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; (i < ((int)FF_FAKE_PATH_MAX) - 1) && (src[i] != '\0'); i++) {
|
||
dst[i] = src[i];
|
||
}
|
||
dst[i] = '\0';
|
||
}
|
||
|
||
/* 同 s_keep,但目标容量由调用方给(FILINFO.fname 是 13 字节,与路径上限不同) */
|
||
static void s_keep_n(char *dst, int dst_sz, const char *src)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; (i < (dst_sz - 1)) && (src[i] != '\0'); i++) {
|
||
dst[i] = src[i];
|
||
}
|
||
dst[i] = '\0';
|
||
}
|
||
|
||
/* ─────────────────────────── 世界控制 ─────────────────────────── */
|
||
|
||
void ff_fake_reset(void)
|
||
{
|
||
(void)memset(&g_ff_obs, 0, sizeof(g_ff_obs));
|
||
(void)memset(s_ent, 0, sizeof(s_ent));
|
||
(void)memset(s_inj, 0, sizeof(s_inj));
|
||
(void)memset(s_dirty, 0, sizeof(s_dirty));
|
||
s_dostime_d = 0x5A21u;
|
||
s_dostime_t = 0x6E40u;
|
||
}
|
||
|
||
void ff_fake_set_dostime(uint16_t fdate, uint16_t ftime)
|
||
{
|
||
s_dostime_d = fdate;
|
||
s_dostime_t = ftime;
|
||
}
|
||
|
||
static void s_add_inj_skip(const char *path, int op, int act, FRESULT rc, UINT n, int skip, int times)
|
||
{
|
||
int i;
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
|
||
if (times <= 0) {
|
||
return;
|
||
}
|
||
if (path == NULL) {
|
||
for (i = 0; i < (int)FF_FAKE_INJ_MAX; i++) {
|
||
if (s_inj[i].used == 0) {
|
||
s_inj[i].used = 1;
|
||
s_inj[i].any_path = 1;
|
||
s_inj[i].op = op;
|
||
s_inj[i].act = act;
|
||
s_inj[i].rc = rc;
|
||
s_inj[i].n = n;
|
||
s_inj[i].times = times;
|
||
s_inj[i].skip = skip;
|
||
s_inj[i].path[0] = '\0';
|
||
return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
/* 注入路径也走一遍归一化,好让大小写/前导斜杠的差异不影响命中 */
|
||
if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) {
|
||
norm[0] = '/';
|
||
norm[1] = '\0';
|
||
}
|
||
for (i = 0; i < (int)FF_FAKE_INJ_MAX; i++) {
|
||
if (s_inj[i].used == 0) {
|
||
s_inj[i].used = 1;
|
||
s_inj[i].any_path = 0;
|
||
s_inj[i].op = op;
|
||
s_inj[i].act = act;
|
||
s_inj[i].rc = rc;
|
||
s_inj[i].n = n;
|
||
s_inj[i].times = times;
|
||
s_inj[i].skip = skip;
|
||
s_keep(s_inj[i].path, norm);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void ff_fake_inject_rc(const char *path, int op, FRESULT rc, int times)
|
||
{
|
||
s_add_inj_skip(path, op, FF_FAKE_ACT_RC, rc, 0u, 0, times);
|
||
}
|
||
|
||
/* 前 skip 次命中放行,之后 times 次注入 rc(见头文件注释的用途说明) */
|
||
void ff_fake_inject_rc_skip(const char *path, int op, FRESULT rc, int skip, int times)
|
||
{
|
||
s_add_inj_skip(path, op, FF_FAKE_ACT_RC, rc, 0u, skip, times);
|
||
}
|
||
|
||
void ff_fake_inject_write_short(const char *path, UINT n, int times)
|
||
{
|
||
s_add_inj_skip(path, FF_FAKE_OP_WRITE, FF_FAKE_ACT_WSHORT, FR_OK, n, 0, times);
|
||
}
|
||
|
||
void ff_fake_inject_read_short(const char *path, UINT n, int times)
|
||
{
|
||
s_add_inj_skip(path, FF_FAKE_OP_READ, FF_FAKE_ACT_RSHORT, FR_OK, n, 0, times);
|
||
}
|
||
|
||
/* ─────────────────────────── 卷直读直写 ─────────────────────────── */
|
||
|
||
int ff_fake_exists(const char *path)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
int idx;
|
||
|
||
if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) {
|
||
return 0;
|
||
}
|
||
idx = s_find(norm);
|
||
if (idx < 0) {
|
||
return 0;
|
||
}
|
||
return (s_ent[idx].is_dir != 0) ? 2 : 1;
|
||
}
|
||
|
||
uint32_t ff_fake_size(const char *path)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
int idx;
|
||
|
||
if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) {
|
||
return 0xFFFFFFFFu;
|
||
}
|
||
idx = s_find(norm);
|
||
return (idx < 0) ? 0xFFFFFFFFu : s_ent[idx].size;
|
||
}
|
||
|
||
uint32_t ff_fake_time_token(const char *path)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
int idx;
|
||
|
||
if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) {
|
||
return 0u;
|
||
}
|
||
idx = s_find(norm);
|
||
if (idx < 0) {
|
||
return 0u;
|
||
}
|
||
return ((uint32_t)s_ent[idx].fdate << 16) | (uint32_t)s_ent[idx].ftime;
|
||
}
|
||
|
||
uint32_t ff_fake_fetch(const char *path, uint8_t *p_buf, uint32_t off, uint32_t cap)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
int idx;
|
||
uint32_t n;
|
||
|
||
if ((p_buf == NULL) || (s_norm(path, norm, (int)sizeof(norm)) != FR_OK)) {
|
||
return 0u;
|
||
}
|
||
idx = s_find(norm);
|
||
if (idx < 0) {
|
||
return 0u;
|
||
}
|
||
if (off >= s_ent[idx].stored) {
|
||
return 0u;
|
||
}
|
||
n = s_ent[idx].stored - off;
|
||
if (n > cap) {
|
||
n = cap;
|
||
}
|
||
(void)memcpy(p_buf, &s_ent[idx].data[off], n);
|
||
return n;
|
||
}
|
||
|
||
int ff_fake_mkfile(const char *path, const uint8_t *p_data, uint32_t len)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
int idx;
|
||
|
||
if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) {
|
||
return -1;
|
||
}
|
||
idx = s_find(norm);
|
||
if (idx < 0) {
|
||
idx = s_alloc();
|
||
if (idx < 0) {
|
||
return -1;
|
||
}
|
||
s_ent[idx].used = 1;
|
||
s_ent[idx].is_dir = 0;
|
||
s_keep(s_ent[idx].path, norm);
|
||
}
|
||
if (len > FF_FAKE_CLUSTER) {
|
||
len = FF_FAKE_CLUSTER;
|
||
}
|
||
s_ent[idx].stored = len;
|
||
s_ent[idx].size = len;
|
||
if (len > 0u) {
|
||
if (p_data != NULL) {
|
||
(void)memcpy(s_ent[idx].data, p_data, len);
|
||
} else {
|
||
/* p_data == NULL = "只铺长度、内容清零":造"已有 N 条记录"的前置状态
|
||
* 时用这条,不必真把 N*64 字节写进池子 */
|
||
(void)memset(s_ent[idx].data, 0, len);
|
||
}
|
||
}
|
||
s_ent[idx].fdate = s_dostime_d;
|
||
s_ent[idx].ftime = s_dostime_t;
|
||
return 0;
|
||
}
|
||
|
||
/* 只给逻辑长度:造"已有 1024 条记录 = 65536 字节"这类前置状态,
|
||
* 内容不真存(簇池只有 2 KB),读到的超出部分按 0 处理 */
|
||
int ff_fake_mkfile_size(const char *path, uint32_t size)
|
||
{
|
||
if (ff_fake_mkfile(path, NULL, 0u) != 0) {
|
||
return -1;
|
||
}
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
int idx;
|
||
|
||
(void)s_norm(path, norm, (int)sizeof(norm));
|
||
idx = s_find(norm);
|
||
if (idx < 0) {
|
||
return -1;
|
||
}
|
||
s_ent[idx].size = size;
|
||
s_ent[idx].stored = 0u;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
int ff_fake_mkdir_direct(const char *path)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
int idx;
|
||
|
||
if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) {
|
||
return -1;
|
||
}
|
||
idx = s_find(norm);
|
||
if (idx >= 0) {
|
||
s_ent[idx].is_dir = 1;
|
||
return 0;
|
||
}
|
||
idx = s_alloc();
|
||
if (idx < 0) {
|
||
return -1;
|
||
}
|
||
s_ent[idx].used = 1;
|
||
s_ent[idx].is_dir = 1;
|
||
s_ent[idx].size = 0u;
|
||
s_ent[idx].stored = 0u;
|
||
s_keep(s_ent[idx].path, norm);
|
||
return 0;
|
||
}
|
||
|
||
int ff_fake_count_entries(void)
|
||
{
|
||
int i;
|
||
int n = 0;
|
||
|
||
for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) {
|
||
if (s_ent[i].used != 0) {
|
||
n++;
|
||
}
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* ─────────────────────────── f_* 实现 ─────────────────────────── */
|
||
|
||
static void s_inval(FIL *fp)
|
||
{
|
||
fp->obj.fs = NULL; /* 真实 ff.c:失败即 fp->obj.fs = 0 */
|
||
fp->obj.sclust = 0u;
|
||
fp->obj.objsize = 0u;
|
||
fp->fptr = 0u;
|
||
fp->flag = 0u;
|
||
}
|
||
|
||
FRESULT f_open(FIL *fp, const TCHAR *path, BYTE mode)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
FRESULT fr;
|
||
FRESULT inj_rc = FR_OK;
|
||
int inj_act = 0;
|
||
UINT inj_n = 0u;
|
||
int hit = 0;
|
||
int idx;
|
||
|
||
s_guard(FF_FAKE_OP_OPEN);
|
||
if (fp == NULL) {
|
||
return FR_INVALID_OBJECT;
|
||
}
|
||
s_inval(fp);
|
||
fr = s_norm(path, norm, (int)sizeof(norm));
|
||
if (fr != FR_OK) {
|
||
g_ff_obs.last_open_rc = (int)fr;
|
||
return fr;
|
||
}
|
||
hit = s_hit(FF_FAKE_OP_OPEN, norm, &inj_rc, &inj_act, &inj_n);
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) {
|
||
g_ff_obs.last_open_mode = mode;
|
||
s_keep(g_ff_obs.last_open_path, norm);
|
||
g_ff_obs.last_open_rc = (int)inj_rc;
|
||
return inj_rc;
|
||
}
|
||
g_ff_obs.last_open_mode = mode;
|
||
s_keep(g_ff_obs.last_open_path, norm);
|
||
|
||
idx = s_find(norm);
|
||
if (idx >= 0) { /* 对象已存在 */
|
||
if ((mode & FA_CREATE_NEW) != 0u) {
|
||
g_ff_obs.last_open_rc = (int)FR_EXIST;
|
||
return FR_EXIST; /* 真实 f_open:Cannot create as new file */
|
||
}
|
||
if (s_ent[idx].is_dir != 0) { /* 对目录本身开文件 */
|
||
if ((mode & FF_FAKE_CREATE_MASK) != 0u) {
|
||
g_ff_obs.last_open_rc = (int)FR_DENIED; /* 真实:Cannot overwrite (DIR) */
|
||
return FR_DENIED;
|
||
}
|
||
g_ff_obs.last_open_rc = (int)FR_NO_FILE; /* 真实 f_open:3850 */
|
||
return FR_NO_FILE;
|
||
}
|
||
if ((mode & FA_CREATE_ALWAYS) != 0u) {
|
||
s_ent[idx].size = 0u; /* 截零(真实 ff.c 复位目录项 + 删簇链) */
|
||
s_ent[idx].stored = 0u;
|
||
s_dirty[idx] = 1u;
|
||
}
|
||
} else { /* 对象不存在 */
|
||
/* 真实 ff.c:中间目录段不存在时 follow_path 就已经是 FR_NO_PATH 了,
|
||
* **先判父目录再判创建位**,否则 "/log 未建 + 只给 FA_READ" 会被误报成
|
||
* FR_NO_FILE,被测代码的自愈分支就走不到 */
|
||
fr = s_check_parent(norm);
|
||
if (fr != FR_OK) {
|
||
g_ff_obs.last_open_rc = (int)fr;
|
||
return fr;
|
||
}
|
||
if ((mode & FF_FAKE_CREATE_MASK) == 0u) {
|
||
g_ff_obs.last_open_rc = (int)FR_NO_FILE;
|
||
return FR_NO_FILE;
|
||
}
|
||
idx = s_alloc();
|
||
if (idx < 0) {
|
||
g_ff_obs.last_open_rc = (int)FR_DENIED; /* 目录满(真实 dir_register) */
|
||
return FR_DENIED;
|
||
}
|
||
s_ent[idx].used = 1;
|
||
s_ent[idx].is_dir = 0;
|
||
s_ent[idx].size = 0u;
|
||
s_ent[idx].stored = 0u;
|
||
s_keep(s_ent[idx].path, norm);
|
||
s_dirty[idx] = 1u;
|
||
}
|
||
|
||
/* 句柄:mode 原值进 flag(真实 ff.c 同做法),因此"只给 FA_OPEN_APPEND"
|
||
* 的句柄在 f_write 里必然拿 FR_DENIED —— 陷阱一的复刻度量 */
|
||
fp->obj.fs = FF_FAKE_FS_MAGIC;
|
||
fp->obj.sclust = (DWORD)idx + 1u;
|
||
fp->obj.objsize = (FSIZE_t)s_ent[idx].size;
|
||
fp->flag = mode;
|
||
fp->err = 0u;
|
||
fp->fptr = ((mode & FF_FAKE_SEEKEND) != 0u) ? (FSIZE_t)s_ent[idx].size : 0u;
|
||
g_ff_obs.last_open_rc = (int)FR_OK;
|
||
return FR_OK;
|
||
}
|
||
|
||
static int s_chk(FIL *fp, int *p_idx)
|
||
{
|
||
if ((fp == NULL) || (fp->obj.fs != FF_FAKE_FS_MAGIC)) {
|
||
return 0; /* FR_INVALID_OBJECT */
|
||
}
|
||
if ((fp->obj.sclust == 0u) || (fp->obj.sclust > (DWORD)FF_FAKE_MAX_ENT)) {
|
||
return 0;
|
||
}
|
||
*p_idx = (int)fp->obj.sclust - 1;
|
||
if (s_ent[*p_idx].used == 0) {
|
||
return 0;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
FRESULT f_write(FIL *fp, const void *p_buff, UINT btw, UINT *bw)
|
||
{
|
||
FRESULT fr;
|
||
FRESULT inj_rc = FR_OK;
|
||
int inj_act = 0;
|
||
UINT inj_n = 0u;
|
||
int hit = 0;
|
||
int idx = -1;
|
||
uint32_t pos;
|
||
uint32_t room;
|
||
uint32_t n;
|
||
|
||
if (bw != NULL) {
|
||
*bw = 0u;
|
||
}
|
||
s_guard(FF_FAKE_OP_WRITE);
|
||
g_ff_obs.last_write_btw = btw;
|
||
g_ff_obs.last_write_bw = 0u;
|
||
if (!s_chk(fp, &idx)) {
|
||
g_ff_obs.last_write_rc = (int)FR_INVALID_OBJECT;
|
||
return FR_INVALID_OBJECT;
|
||
}
|
||
/* 陷阱一:只给 FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02) -> 恒返 FR_DENIED */
|
||
if ((fp->flag & FA_WRITE) == 0u) {
|
||
g_ff_obs.last_write_rc = (int)FR_DENIED;
|
||
return FR_DENIED;
|
||
}
|
||
hit = s_hit(FF_FAKE_OP_WRITE, s_ent[idx].path, &inj_rc, &inj_act, &inj_n);
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) {
|
||
g_ff_obs.last_write_rc = (int)inj_rc;
|
||
return inj_rc;
|
||
}
|
||
if (p_buff == NULL) {
|
||
g_ff_obs.last_write_rc = (int)FR_INT_ERR;
|
||
return FR_INT_ERR;
|
||
}
|
||
|
||
pos = (uint32_t)fp->fptr;
|
||
/* pos 可能已经越过簇池上沿(ff_fake_mkfile_size 造的"已有 64 KB 记录"就是这种),
|
||
* 等价于"一个簇都分配不出来":短写到 0 字节,仍然 FR_OK */
|
||
room = (pos >= FF_FAKE_CLUSTER) ? 0u : (FF_FAKE_CLUSTER - pos);
|
||
n = btw;
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_WSHORT)) {
|
||
if (n > inj_n) {
|
||
n = inj_n; /* 定点短写:恰好写 inj_n 字节后"当成功"返回 */
|
||
}
|
||
}
|
||
if (n > room) {
|
||
n = room; /* 真实 ff.c:4102 clst == 0 -> break:FR_OK + 短写 */
|
||
}
|
||
if (n > 0u) {
|
||
(void)memcpy(&s_ent[idx].data[pos], p_buff, n);
|
||
if ((pos + n) > s_ent[idx].stored) {
|
||
s_ent[idx].stored = pos + n;
|
||
}
|
||
}
|
||
fp->fptr = (FSIZE_t)(pos + n);
|
||
if (fp->fptr > fp->obj.objsize) {
|
||
fp->obj.objsize = fp->fptr;
|
||
}
|
||
s_ent[idx].size = (uint32_t)fp->obj.objsize;
|
||
s_dirty[idx] = 1u;
|
||
if (bw != NULL) {
|
||
*bw = n;
|
||
}
|
||
g_ff_obs.last_write_bw = n;
|
||
g_ff_obs.last_write_rc = (int)FR_OK;
|
||
fr = FR_OK; /* 短写也是 FR_OK:判失败只能靠 *bw(§10.1 第 11 条) */
|
||
return fr;
|
||
}
|
||
|
||
FRESULT f_read(FIL *fp, void *p_buff, UINT btr, UINT *br)
|
||
{
|
||
FRESULT inj_rc = FR_OK;
|
||
int inj_act = 0;
|
||
UINT inj_n = 0u;
|
||
int hit = 0;
|
||
int idx = -1;
|
||
uint32_t pos;
|
||
uint32_t remain;
|
||
uint32_t n;
|
||
|
||
if (br != NULL) {
|
||
*br = 0u;
|
||
}
|
||
s_guard(FF_FAKE_OP_READ);
|
||
g_ff_obs.last_read_btr = btr;
|
||
g_ff_obs.last_read_br = 0u;
|
||
if (!s_chk(fp, &idx)) {
|
||
return FR_INVALID_OBJECT;
|
||
}
|
||
if ((fp->flag & FA_READ) == 0u) { /* 真实 f_read:Check access mode */
|
||
return FR_DENIED;
|
||
}
|
||
hit = s_hit(FF_FAKE_OP_READ, s_ent[idx].path, &inj_rc, &inj_act, &inj_n);
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) {
|
||
return inj_rc;
|
||
}
|
||
if (p_buff == NULL) {
|
||
return FR_INT_ERR;
|
||
}
|
||
pos = (uint32_t)fp->fptr;
|
||
remain = ((uint32_t)fp->obj.objsize > pos) ? ((uint32_t)fp->obj.objsize - pos) : 0u;
|
||
n = btr;
|
||
if (n > remain) {
|
||
n = remain; /* 真实 f_read:btr 被 remain 截断后照常 FR_OK */
|
||
}
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RSHORT) && (n > inj_n)) {
|
||
n = inj_n;
|
||
}
|
||
{
|
||
uint32_t k;
|
||
uint8_t *dst = (uint8_t *)p_buff;
|
||
|
||
for (k = 0u; k < n; k++) {
|
||
dst[k] = ((pos + k) < s_ent[idx].stored) ? s_ent[idx].data[pos + k] : 0u;
|
||
}
|
||
}
|
||
fp->fptr = (FSIZE_t)(pos + n);
|
||
if (br != NULL) {
|
||
*br = n;
|
||
}
|
||
g_ff_obs.last_read_br = n;
|
||
return FR_OK;
|
||
}
|
||
|
||
FRESULT f_lseek(FIL *fp, FSIZE_t ofs)
|
||
{
|
||
int idx = -1;
|
||
uint32_t want = (uint32_t)ofs;
|
||
|
||
s_guard(FF_FAKE_OP_LSEEK);
|
||
g_ff_obs.last_lseek_ofs = ofs;
|
||
if (!s_chk(fp, &idx)) {
|
||
return FR_INVALID_OBJECT;
|
||
}
|
||
if (want > (uint32_t)fp->obj.objsize) {
|
||
if ((fp->flag & FA_WRITE) == 0u) {
|
||
want = (uint32_t)fp->obj.objsize; /* 只读句柄:钳到文件尾,不报错 */
|
||
} else if (want > FF_FAKE_CLUSTER) {
|
||
return FR_DENIED; /* 可写但要超出簇池 = 簇分配不出来 */
|
||
} else {
|
||
s_ent[idx].size = want; /* 可写句柄:扩展(真实 ff.c 补零簇链) */
|
||
fp->obj.objsize = (FSIZE_t)want;
|
||
s_dirty[idx] = 1u;
|
||
}
|
||
}
|
||
fp->fptr = (FSIZE_t)want;
|
||
return FR_OK;
|
||
}
|
||
|
||
FRESULT f_close(FIL *fp)
|
||
{
|
||
int idx = -1;
|
||
FRESULT inj_rc = FR_OK;
|
||
int inj_act = 0;
|
||
UINT inj_n = 0u;
|
||
int hit = 0;
|
||
|
||
s_guard(FF_FAKE_OP_CLOSE);
|
||
if (!s_chk(fp, &idx)) {
|
||
return FR_INVALID_OBJECT;
|
||
}
|
||
hit = s_hit(FF_FAKE_OP_CLOSE, s_ent[idx].path, &inj_rc, &inj_act, &inj_n);
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) {
|
||
return inj_rc; /* 真实 f_close 会因 sync 失败返错且释放句柄 */
|
||
}
|
||
if (s_dirty[idx] != 0u) { /* 真实 f_close:sync 里用 get_fattime 回写目录项 */
|
||
s_ent[idx].fdate = s_dostime_d;
|
||
s_ent[idx].ftime = s_dostime_t;
|
||
s_dirty[idx] = 0u;
|
||
}
|
||
s_inval(fp);
|
||
return FR_OK;
|
||
}
|
||
|
||
FRESULT f_stat(const TCHAR *path, FILINFO *fno)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
FRESULT fr;
|
||
FRESULT inj_rc = FR_OK;
|
||
int inj_act = 0;
|
||
UINT inj_n = 0u;
|
||
int hit = 0;
|
||
int idx;
|
||
|
||
s_guard(FF_FAKE_OP_STAT);
|
||
fr = s_norm(path, norm, (int)sizeof(norm));
|
||
if (fr != FR_OK) {
|
||
return fr;
|
||
}
|
||
hit = s_hit(FF_FAKE_OP_STAT, norm, &inj_rc, &inj_act, &inj_n);
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) {
|
||
return inj_rc;
|
||
}
|
||
idx = s_find(norm);
|
||
if (idx < 0) {
|
||
/* 真实 follow_path:末段找不到 -> FR_NO_FILE,但**中间段**找不到 -> FR_NO_PATH */
|
||
fr = s_check_parent(norm);
|
||
return (fr == FR_OK) ? FR_NO_FILE : fr;
|
||
}
|
||
if (fno != NULL) {
|
||
(void)memset(fno, 0, sizeof(*fno));
|
||
fno->fsize = (FSIZE_t)s_ent[idx].size;
|
||
fno->fdate = s_ent[idx].fdate;
|
||
fno->ftime = s_ent[idx].ftime;
|
||
fno->fattrib = (s_ent[idx].is_dir != 0) ? AM_DIR : AM_ARC;
|
||
{
|
||
const char *base = strrchr(norm, '/');
|
||
base = (base != NULL) ? (base + 1) : norm;
|
||
s_keep_n(fno->fname, (int)sizeof(fno->fname), base);
|
||
}
|
||
}
|
||
return FR_OK;
|
||
}
|
||
|
||
FRESULT f_unlink(const TCHAR *path)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
FRESULT fr;
|
||
FRESULT inj_rc = FR_OK;
|
||
int inj_act = 0;
|
||
UINT inj_n = 0u;
|
||
int hit = 0;
|
||
int idx;
|
||
|
||
s_guard(FF_FAKE_OP_UNLINK);
|
||
fr = s_norm(path, norm, (int)sizeof(norm));
|
||
if (fr != FR_OK) {
|
||
return fr;
|
||
}
|
||
s_keep(g_ff_obs.last_unlink_path, norm);
|
||
hit = s_hit(FF_FAKE_OP_UNLINK, norm, &inj_rc, &inj_act, &inj_n);
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) {
|
||
return inj_rc;
|
||
}
|
||
idx = s_find(norm);
|
||
if (idx < 0) {
|
||
fr = s_check_parent(norm);
|
||
return (fr == FR_OK) ? FR_NO_FILE : fr;
|
||
}
|
||
if ((s_ent[idx].is_dir != 0) && (s_has_children(norm) != 0)) {
|
||
return FR_DENIED; /* 真实 f_unlink:目录非空不可删 */
|
||
}
|
||
s_ent[idx].used = 0;
|
||
s_dirty[idx] = 0u;
|
||
return FR_OK;
|
||
}
|
||
|
||
FRESULT f_mkdir(const TCHAR *path)
|
||
{
|
||
char norm[FF_FAKE_PATH_MAX];
|
||
FRESULT fr;
|
||
FRESULT inj_rc = FR_OK;
|
||
int inj_act = 0;
|
||
UINT inj_n = 0u;
|
||
int hit = 0;
|
||
int idx;
|
||
|
||
s_guard(FF_FAKE_OP_MKDIR);
|
||
fr = s_norm(path, norm, (int)sizeof(norm));
|
||
if (fr != FR_OK) {
|
||
return fr;
|
||
}
|
||
s_keep(g_ff_obs.last_mkdir_path, norm);
|
||
hit = s_hit(FF_FAKE_OP_MKDIR, norm, &inj_rc, &inj_act, &inj_n);
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) {
|
||
return inj_rc;
|
||
}
|
||
if (s_find(norm) >= 0) {
|
||
return FR_EXIST; /* 真实 f_mkdir:已存在 -> FR_EXIST */
|
||
}
|
||
fr = s_check_parent(norm);
|
||
if (fr != FR_OK) {
|
||
return fr;
|
||
}
|
||
idx = s_alloc();
|
||
if (idx < 0) {
|
||
return FR_DENIED;
|
||
}
|
||
s_ent[idx].used = 1;
|
||
s_ent[idx].is_dir = 1;
|
||
s_ent[idx].size = 0u;
|
||
s_ent[idx].stored = 0u;
|
||
s_ent[idx].fdate = s_dostime_d;
|
||
s_ent[idx].ftime = s_dostime_t;
|
||
s_keep(s_ent[idx].path, norm);
|
||
return FR_OK;
|
||
}
|
||
|
||
FRESULT f_rename(const TCHAR *path_old, const TCHAR *path_new)
|
||
{
|
||
char nold[FF_FAKE_PATH_MAX];
|
||
char nnew[FF_FAKE_PATH_MAX];
|
||
FRESULT fr;
|
||
FRESULT inj_rc = FR_OK;
|
||
int inj_act = 0;
|
||
UINT inj_n = 0u;
|
||
int hit = 0;
|
||
int idx;
|
||
int dup;
|
||
|
||
s_guard(FF_FAKE_OP_RENAME);
|
||
fr = s_norm(path_old, nold, (int)sizeof(nold));
|
||
if (fr != FR_OK) {
|
||
return fr;
|
||
}
|
||
fr = s_norm(path_new, nnew, (int)sizeof(nnew));
|
||
if (fr != FR_OK) {
|
||
return fr;
|
||
}
|
||
s_keep(g_ff_obs.last_rename_old, nold);
|
||
s_keep(g_ff_obs.last_rename_new, nnew);
|
||
hit = s_hit(FF_FAKE_OP_RENAME, nold, &inj_rc, &inj_act, &inj_n);
|
||
if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) {
|
||
return inj_rc;
|
||
}
|
||
idx = s_find(nold);
|
||
if (idx < 0) {
|
||
fr = s_check_parent(nold);
|
||
return (fr == FR_OK) ? FR_NO_FILE : fr;
|
||
}
|
||
dup = s_find(nnew);
|
||
if (dup >= 0) {
|
||
/* E-01 台架实证的根因:本移植(与上游同语义)"new name already in use"
|
||
* 一律 FR_EXIST、不覆盖。原子写因此必须先 unlink 目标再 rename。 */
|
||
return FR_EXIST;
|
||
}
|
||
fr = s_check_parent(nnew);
|
||
if (fr != FR_OK) {
|
||
return fr;
|
||
}
|
||
if (s_ent[idx].is_dir != 0) { /* 目录改名:同步搬走全部子项 */
|
||
int dl = (int)strlen(nold);
|
||
int i;
|
||
|
||
for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) {
|
||
if ((s_ent[i].used == 0) || (i == idx)) {
|
||
continue;
|
||
}
|
||
if (((int)strlen(s_ent[i].path) > dl) &&
|
||
(strncmp(s_ent[i].path, nold, (size_t)dl) == 0) &&
|
||
(s_ent[i].path[dl] == '/')) {
|
||
char tmp[FF_FAKE_PATH_MAX];
|
||
int m;
|
||
|
||
for (m = 0; nnew[m] != '\0'; m++) {
|
||
tmp[m] = nnew[m];
|
||
}
|
||
s_keep_n(&tmp[m], (int)sizeof(tmp) - m, &s_ent[i].path[dl]);
|
||
s_keep(s_ent[i].path, tmp);
|
||
}
|
||
}
|
||
}
|
||
s_keep(s_ent[idx].path, nnew);
|
||
s_ent[idx].fdate = s_dostime_d;
|
||
s_ent[idx].ftime = s_dostime_t;
|
||
return FR_OK;
|
||
}
|