0f7b5a87fb
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
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) {
|
||
/* 台架实证的根因:本移植(与上游同语义)"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;
|
||
}
|