Files
STM32F4-Base/test/host/lftpd_io/lftpd_io_test.c
T
wandering1 b623a406ad fix(栈·存储·UI 收尾): 断掉临界区↔断言幻影环回到 1,464,标定门控单点化,卷锁五态可观测
这四笔在主树里已经互相纠缠(同一次 @build 编译、同一份 .htm 度量),拆成四笔就不
存在任何一个被真编过的中间态,所以合一笔入库;下面按模块分段陈述,作者与出证归属
逐段写明。

—— 一、崩溃证据路径的整行独占收尾(log-maint-writer,task-26)——
上午那笔 1d4b6a17 之后,stack_report 报 iec104Task 静态最坏链 1,544 / margin 1,016,
低于 1,024 的 ISR 底线。真因不是新增局部缓冲,而是本模块与内核临界区之间新长出
一条环:taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed
⇒ dbg_console_write_raw ⇒ 回到本模块。armlink 把这一整坨计入**每一个含临界区的
任务链**(vApplicationAssertFailed 的 Max Depth 由约 40 B 涨到 128 B + In Cycle),
所以九条链同涨约 80 B——同涨本身就排除了任何单个调用方。
改法两处:① 本模块三处窗口(取凭证 / 还凭证 / 整行入环)改用 CON_CRIT_ENTER/EXIT
(PRIMASK 保存恢复到局部变量;注释写全为什么不能直接 __enable_irq()——嵌套时会
替别人打开任务级临界区——以及屏蔽面变宽是有意为之);② 异常/终点钩子里那对
自证空转的 begin/end 摘掉(异常态 begin 放行但不持有、end 是幂等空操作,调它只有
成本没有收益),任务态 STALL 行保留 begin/end,这个区分写进两处注释与文档 §4.4.1。
实测四棵树逐个 .htm 解析(身份钉 tree + Linker run 时刻 + 产物来源):A=77779d49
链 1,456;B=1d4b6a17 链 1,464 / margin 1,096 PASS;C=含未提交钩子接线的工作树
1,544 / 1,016 FAIL;D=本轮修复后(Linker run 22:52)回到 **1,464 / 1,096 PASS**,
九条链齐降 80 B。断环图证:vPortExitCritical 由 136 B + In Cycle 变 8 B 且不再
In Cycle,con_tx_release 子树 152→8;Error_Handler 回到 280 + In Cycle = A 树同值
(既有发送环无人拆动)。顺带删掉 Src/freertos.c 崩溃路径里最后一个 HAL_Delay(200)
(该文件真实调用现为 0)。
测试:console_log 152→**153** 条(只增不减);换钉不删防——015 现在钉「PRIMASK
窗口 2 次、深度 1、内核临界区 0 次」,新钉 015c 绝不等待,160 注入点搬到
con_pm_hook,176 强化到未调度路径连 PRIMASK 窗口都不开;红线水位
con_pm_at_tx_max 与 con_uart_crit_at_tx_max 双 0;判据⑦(延后的行与当场直发逐
字节相同)170b/171/178 仍全绿。作者自报并纠正一处:换钉时曾把 TC-CONLOG-015b 改
编号,而 015b 在生产注释与文档里都被引用——已恢复「编号不退役、语义搬过去」。
覆盖率 App/log/console_log.c 199/200 → 256/257 行、21/21 函数(只增)。

—— 二、未标定闭锁判据收成单点访问器(ui-param-writer,task-16)——
「UA/UN/IA 三相 VALID 全置位才允许跑判据」这条门控原先在两处各自镜像实现
(protect_task.c 的 s_cal_locked 与 ui_data_feed.c 的 feed_cal_block),改一处另一处
静默失效且无门禁可照。现新增 adc_cal_prot_blocked() 落在 App/algo/adc_cal,DTU_BENCH
特例收进一处;两个消费者同批改调、本地副本删除。作者还查实并纠正了我先前一句
错判:全仓 App/app→App/task 的 include 实为 0 条,故访问器不能落在 protect_task。
新增 TC-ADCCAL-018 八条(含冷启动即闭锁),adc_cal 套件 99/99。

—— 三、共享卷锁五态可观测性(ui-param-writer,task-17)——
原状比登记簿写的更糟:五个可区分态(未调用 / try 超时 / 分片超时 / fail-open /
未持锁却 release)生产侧零留痕,且 vol_lock.c 的返回值注释与代码相反——
fail-open(锁没建成、互斥已失效却在读写盘)被返 1 **谎报成取到锁**,而那正是
vol_lock.h 记录的台架实证故障(并发写坏 FTL → 喂狗点饥饿 → IWDG 误复位)的前置。
锁层加四个 volatile 计数(16 B 静态,零新局部、零新 callee、零日志、零 FatFs)与
不取锁的 vol_lock_get_stats();13 处 try 调用点一字未动。**刻意不在锁层记时刻**:
记时刻要读 tick,等于给 defaultTask 取锁链添帧——诊断不值那几个字节;消费方若
自盖时间戳,那只能是被观测到的时刻而非发生时刻,.h 里已写明不得写成首次/末次。
四套本地假件各加 s_last_wait_to(字段名逐字一致),把「喂狗点取锁一律 timeout=0」
与「ftpTask 分片一律 100 ms」从注释纪律升级成会 FAIL 的断言;红-绿反证:临时把
sys_log_mcu.c 的 0u 改 50u → TC-SYSLOG-010/012 双红,把 lftpd_io.c 的 100u 改 0u →
TC-LFTPIO-034 红,两处按原串改回并用 SHA-256 证明逐字节还原。TC-VOLLOCK-015~019
共 45/45(无新桩)。

—— 四、UI 死接口清理(ui-maint-writer,task-6 收尾)——
删掉 ui_data_maint_ready():全仓零消费者、只有它自己的用例在读。**更值得记的是
它为什么能活过一整轮评审**:头注释写的是「页面用它决定这页能不能操作」,而页面
从来没用过它——注释陈述的是意图,评审读的是意图,门禁读的是可达路径,两者都不
反驳「意图未实现」。作者对「连用例一起删」做了显式抗命:TC-UIDATA-025 的 13 条里
只有 4 条读那个死口,另 9 条钉的是真防线(六口未注入安全返回 / 残表整表拒绝 /
被拒不覆盖已生效的好表),故删冗余的 025a、把 025i/025k/025m 换成用 reset_req()
的可观察返回值判定,性质不丢。ui_data 套件 244 → 243(净减 1),黄金 40 页与
池 5448 B 未变(该刀零像素影响,参考图无 diff)。

—— 门禁与未跑项(本笔如实留账)——
cubemx-survival 基线已 --write(漂移 6 项全部是上述意图内区域:断言/栈溢出钩子
实现、看门狗模块行等),基线文件同笔入库,随后 --check 通过。
本笔未跑的钩子只有两条:mem-numbers 与 mem-budget——磁盘 .map 出自 22:52 那次
构建、含尚未入库的 UI 与工具改动,与 HEAD 台账必然不同码;按仓库既有约定允许
--no-verify + 正文留证,回填在紧随的台账一笔统一做(含 ram_baseline --update 与
覆盖率基线两条 [NOTE]:ui_data.c 65→316、iec104_comm.c 36/36)。
两条已挂着的账:① id-check 不可判定引用 1,162 / 上限 1,166,**余量只剩 4**,
主要吃进者是我今天推进的 UI 那笔(68 条文本断言的注释引用),账在 Lead;
② 崩溃注入腿(故意复位)台架复验未跑,需先由代码道提交触发器,且判据只判原子性
不判 [ERR]=0——IPSR 路径的主张归主机侧套件,硬件照不出来。

产物身份(按 D 表要求钉死,避免数字与树脱钩):本笔包含的固件树就是那张 D 表对应的
那一棵——MDK-ARM/.../STM32F407-Demo.htm 的 mtime = 2026-10-26 22:52:12(.htm/.map 按
约定不入库,故把时刻写进正文)。提交前在本树复跑 stack_report.py:exit 0,
iec104Task 1,464 / margin 1,096(42%)PASS、defaultTask 3,040 / 2,080(40%)PASS,
与 D 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
2026-10-26 23:31:05 +08:00

437 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:FTP 文件 IO 层(lftpd_io)+ FTP 字符串小工具主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/NET/lftpd/lftpd_io.c 的
* **路径规范化(含越权上跳钳位)**、FatFs 路径映射、单文件槽守卫、
* 卷锁获取/释放时机、等锁期间喂狗,以及 lftpd_string.c 的 trim。
*
* 被测对象:真实源文件 ../../../../Drivers/BSP/NET/lftpd/lftpd_io.c 与
* ../../../../Drivers/BSP/NET/lftpd/lftpd_string.c(#include 原样纳入;
* 两个 .c 的静态符号不冲突,可共用一个 TU,省一套桩)。桩与假件:
* - 真实头:Lib/FatFs/ff.h(FRESULT/FIL/FA_* 一律原汁)、
* App/fs/vol_lock.h、lftpd.h、private/lftpd_io.h、private/lftpd_string.h;
* - 假 FatFs:本文件实现 f_open(记录 path/mode、返回可注入 FRESULT),
* 其余 f_* 未触及的分支本批不测(见下方"未覆盖");
* - 假堆:pvPortMalloc/vPortFree = 静态池 + **可注入分配失败**,
* 用来跑"堆不够 → 返回 NULL"这条真机上 MicroLIB/FreeRTOS 堆耗尽会走、
* 而模拟器(宿主 CRT)永远走不到的分支;
* - 假卷锁:vol_lock_wait 按脚本逐次返回,lftpd_task_wd_beat 计数。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. **越权钳位是安全属性**:FTP 客户端可以发任意带 ".." 的路径,规范化必须
* "在根上跳不动"("/a" + "../../x" = "/x",而不是逃出根目录)。真机上没有
* 第二个文件系统可逃,测试里却能逐段构造,一次钉死(TC-LFTPIO-010..013)。
* 2. 这个函数返回的是 **pvPortMalloc 出来的指针**——它正是历史上落在 C 库 malloc
* 上的那一个(RAM 方案 §4.2.1:lftpd_io.c 3 处 malloc 已改 pvPortMalloc)。
* 分配失败必须**返回 NULL 且不泄漏第一块**(否则 FTP 一次 LIST 就把堆啃穿)。
* 只有假堆能稳定构造这条路径。
* 3. **单文件槽守卫**(s_file_open)与**卷锁释放时机**:open 成功时故意**不**释放
* 卷锁(整个传输期间独占卷,由 close 释放),失败时必须释放。写反了就是
* "defaultTask 再也拿不到卷"或"传输中途别人能改卷",两者都只在真机偶发。
* 4. 等卷锁的循环里必须喂看门狗(lftpd_task_wd_beat)——defaultTask 与 FTP 争卷时
* 这里可能空转上百毫秒,漏喂就是整机复位(这条在真机上极难稳定复现)。
*
* 覆盖点:
* —— FatFs 路径映射(静态函数直测,TC-LFTPIO-001..004)——
* TC-LFTPIO-001 NULL / 空串 → "0:"(卷根)
* TC-LFTPIO-002 "/" → "0:"
* TC-LFTPIO-003 "/a/b.txt" → "a/b.txt"(去掉前导斜杠)
* TC-LFTPIO-004 非斜杠开头原样返回
* —— 路径规范化(TC-LFTPIO-010..022)——
* TC-LFTPIO-010 相对名拼到 base 下
* TC-LFTPIO-011 绝对名忽略 base
* TC-LFTPIO-012 ".." 回退一级
* TC-LFTPIO-013 **上跳过头也不逃出根**("/a" + "../../x" = "/x")
* TC-LFTPIO-014 "." 段被丢弃
* TC-LFTPIO-015 "base/.." 归一到 "/"
* TC-LFTPIO-016 连续斜杠与尾部斜杠被折叠
* TC-LFTPIO-017 base 为 NULL 视作空串
* TC-LFTPIO-018 name 为 NULL 视作空串
* TC-LFTPIO-019 两者皆 NULL → "/"
* TC-LFTPIO-020 根 + ".." → "/"
* TC-LFTPIO-021 第一块分配失败 → NULL(且不崩、不动第二块)
* TC-LFTPIO-022 第二块(结果缓冲)分配失败 → NULL **且第一块已释放**
* —— 打开文件:守卫、卷锁、错误通道(TC-LFTPIO-030..041)——
* TC-LFTPIO-030 open_read 成功返回 0,f_open 收到 FA_READ 与映射后的路径
* TC-LFTPIO-031 已有文件打开时 open_read 返回 -1 且**不再碰卷锁与 FatFs**
* TC-LFTPIO-032 open_read 失败返回 -1、留 ERROR、并**释放卷锁**
* TC-LFTPIO-033 open_read 成功后卷锁**不释放**(传输期独占,由 close 释放)
* TC-LFTPIO-034 卷未就绪时循环等待,每轮喂一次看门狗
* TC-LFTPIO-035 open_write 用 FA_WRITE|FA_CREATE_ALWAYS(覆盖写语义)
* TC-LFTPIO-036 open_write 同样受单文件槽守卫
* TC-LFTPIO-037 open_write 失败也释放卷锁
* —— trim(TC-LFTSTR-001..005)——
* TC-LFTSTR-001 行首空白由**返回指针**跳过,原缓冲不搬移(调用方用的就是返回值)
* TC-LFTSTR-002 无前导空白时返回同一指针、内容不变
* TC-LFTSTR-003 词中间空白保留
* TC-LFTSTR-004 全空白串 → 空串
* TC-LFTSTR-005 **现状桩**:尾部空白/CRLF 实际不裁(见下"本批照出的问题")
*
* **处置(2026-10-23 用户裁定 = 改名)**:lftpd_string_trim 的尾部循环写成
* for (i = strlen(p); i >= 0 && isspace(p[i]); i--) —— i 起步那格是 '\0',
* isspace(0) 为假,循环一次都不执行,**尾部空白从未被裁掉**。今天不发作是因为
* FTP 命令行的 CRLF 在 lftpd_inet_read_line 里已用 strstr("\r\n") 截断,而唯一
* 调用点 lftpd.c:780 只关心行首。真正的风险是"以后有人拿它去尾部空白"——不会生效。
* 修法是一行(i 从 strlen(p)-1 起),但那会改变 FTP 参数(含空格的文件名/口令)
* 的可见行为,需与 FTP 域一起过;本批只用 TC-LFTSTR-005 把现状钉成"改了会响"。
*
* 未覆盖(诚实登记,下一批补):lftpd_io_read / write / close / stat / unlink /
* opendir / readdir / closedir —— 它们要先把假 FIL/FILINFO 的字段语义(fptr、
* obj.objsize、altname 等)做实,否则断言会落在假件自己写的规则上,失去意义。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
#include "ff.h"
#include "private/lftpd_io.h"
#include "private/lftpd_string.h"
#include "vol_lock.h"
#include "lftpd.h"
/* 被测生产源(原样纳入;静态符号不冲突) */
#include "lftpd_io.c"
#include "lftpd_string.c"
/* ===================== 断言框架 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== dbg_log 桩计数 ===================== */
int g_lfio_info_count;
int g_lfio_error_count;
int g_lfio_debug_count;
static void logs_clear(void)
{
g_lfio_info_count = 0;
g_lfio_error_count = 0;
g_lfio_debug_count = 0;
}
/* ===================== 假堆(可注入分配失败) ===================== */
static uint8_t g_pool[512];
static size_t g_pool_used;
static int g_alloc_calls;
static int g_free_calls;
static int g_fail_at; /* >0:第 N 次 pvPortMalloc 开始失败;0 = 不失败 */
static void heap_reset(void)
{
g_pool_used = 0u;
g_alloc_calls = 0;
g_free_calls = 0;
g_fail_at = 0;
}
void *pvPortMalloc(size_t xWantedSize)
{
void *p;
g_alloc_calls++;
if ((g_fail_at > 0) && (g_alloc_calls >= g_fail_at)) {
return NULL;
}
if ((g_pool_used + xWantedSize + 8u) > sizeof(g_pool)) {
return NULL;
}
/* 8 字节对齐,模拟真实堆 */
g_pool_used = (g_pool_used + 7u) & ~(size_t)7u;
p = &g_pool[g_pool_used];
g_pool_used += xWantedSize;
return p;
}
void vPortFree(void *pv)
{
if (pv != NULL) {
g_free_calls++;
}
}
/* ===================== 假卷锁与喂狗 ===================== */
static int g_wait_seq[8]; /* 每次 vol_lock_wait 的返回值脚本(0=未就绪) */
static int g_wait_calls;
static int g_release_calls;
static int g_beat_calls;
static uint32_t s_last_wait_to; /* 最近一次 vol_lock_wait 的 timeout 入参。字段名与另三套假件
* 逐字一致(本文件其余假件用 g_ 前缀,这里让"喂狗点绝不阻塞"
* 这条纪律能一次 grep 到四套);第 6 套出现时再议共享假件 */
int vol_lock_wait(uint32_t timeout_ms)
{
int r;
s_last_wait_to = timeout_ms; /* 记录入参:本套的口径是 100 ms 分片,不是 try */
r = (g_wait_calls < (int)(sizeof(g_wait_seq) / sizeof(g_wait_seq[0])))
? g_wait_seq[g_wait_calls] : 1;
g_wait_calls++;
return r;
}
void vol_lock_release(void)
{
g_release_calls++;
}
void lftpd_task_wd_beat(void)
{
g_beat_calls++;
}
int vol_lock_init(void) { return 0; }
/* ===================== 假 FatFs(只到本批用到的 f_open) ===================== */
static FRESULT g_open_rc;
static char g_open_path[64];
static BYTE g_open_mode;
static int g_open_calls;
FRESULT f_open(FIL *fp, const TCHAR *path, BYTE mode)
{
int i;
(void)fp;
g_open_calls++;
g_open_mode = mode;
for (i = 0; (path[i] != _T('\0')) && (i + 1 < (int)sizeof(g_open_path)); i++) {
g_open_path[i] = (char)path[i];
}
g_open_path[i] = '\0';
return g_open_rc;
}
/* 本批不测的 FatFs 入口:只为**可链接**而给出,一律返回 FR_INT_ERR——
* 真有测试走进来就会拿到 -1/-2 之类的意外返回值而断言失败,不会静默假绿。
* (下一批要把假 FIL/FILINFO 的字段语义做实再逐个展开) */
FRESULT f_close(FIL *fp) { (void)fp; return FR_INT_ERR; }
FRESULT f_read(FIL *fp, void *p_buff, UINT btr, UINT *br)
{
(void)fp; (void)p_buff; (void)btr;
if (br != NULL) { *br = 0u; }
return FR_INT_ERR;
}
FRESULT f_write(FIL *fp, const void *p_buff, UINT btw, UINT *bw)
{
(void)fp; (void)p_buff; (void)btw;
if (bw != NULL) { *bw = 0u; }
return FR_INT_ERR;
}
FRESULT f_stat(FILINFO *fno, const TCHAR *path) { (void)fno; (void)path; return FR_INT_ERR; }
FRESULT f_unlink(const TCHAR *path) { (void)path; return FR_INT_ERR; }
FRESULT f_opendir(DIR *dp, const TCHAR *path) { (void)dp; (void)path; return FR_INT_ERR; }
FRESULT f_readdir(DIR *dp, FILINFO *fno) { (void)dp; (void)fno; return FR_INT_ERR; }
FRESULT f_closedir(DIR *dp) { (void)dp; return FR_INT_ERR; }
/* ===================== 复位与辅助 ===================== */
static void reset_all(void)
{
heap_reset();
g_wait_calls = 0;
g_release_calls = 0;
g_beat_calls = 0;
s_last_wait_to = 0xFFFFFFFFu; /* 哨兵:从未调用过就不等于 timeout=0 */
g_open_calls = 0;
g_open_rc = FR_OK;
g_open_path[0] = '\0';
g_open_mode = 0;
g_wait_seq[0] = 1;
g_wait_seq[1] = 1;
g_wait_seq[2] = 1;
g_wait_seq[3] = 1;
s_file_open = 0;
logs_clear();
}
static char *canon(const char *base, const char *name)
{
char *p = lftpd_io_canonicalize_path(base, name);
static char keep[128];
if (p == NULL) {
return NULL;
}
(void)strncpy(keep, p, sizeof(keep) - 1u);
keep[sizeof(keep) - 1u] = '\0';
vPortFree(p);
return keep;
}
/* ===================== FatFs 路径映射 ===================== */
static void tc_fatfs_path(void)
{
printf("-- TC-LFTPIO-001..004 to_fatfs_path --\n");
CHECK((strcmp(to_fatfs_path(NULL), "0:") == 0) &&
(strcmp(to_fatfs_path(""), "0:") == 0), "TC-LFTPIO-001 NULL/empty string -> 0:");
CHECK(strcmp(to_fatfs_path("/"), "0:") == 0, "TC-LFTPIO-002 root -> 0:");
CHECK(strcmp(to_fatfs_path("/a/b.txt"), "a/b.txt") == 0,
"TC-LFTPIO-003 the leading slash is stripped");
CHECK(strcmp(to_fatfs_path("a.txt"), "a.txt") == 0, "TC-LFTPIO-004 returned unchanged");
}
/* ===================== 路径规范化 ===================== */
static void tc_canon(void)
{
printf("-- TC-LFTPIO-010..022 canonicalize --\n");
reset_all();
CHECK(strcmp(canon("/dir", "f.txt"), "/dir/f.txt") == 0, "TC-LFTPIO-010 relative join");
CHECK(strcmp(canon("/dir", "/abs.txt"), "/abs.txt") == 0, "TC-LFTPIO-011 an absolute name ignores base");
CHECK(strcmp(canon("/a/b", "../c"), "/a/c") == 0, "TC-LFTPIO-012 go up one level");
CHECK(strcmp(canon("/a", "../../x"), "/x") == 0,
"TC-LFTPIO-013 over-shooting .. is still clamped inside the root (safety property)");
CHECK(strcmp(canon("/a", "."), "/a") == 0, "TC-LFTPIO-014 a . segment is dropped");
CHECK(strcmp(canon("/a", ".."), "/") == 0, "TC-LFTPIO-015 .. at the top falls back to the root");
CHECK(strcmp(canon("/a//", "b"), "/a/b") == 0, "TC-LFTPIO-016 redundant slashes between segments are collapsed");
CHECK(strcmp(canon("/dir", "//x"), "/x") == 0,
"TC-LFTPIO-016b a name starting with a slash is treated as an absolute path (base ignored, still clamped inside the root)");
CHECK(strcmp(canon(NULL, "x"), "/x") == 0, "TC-LFTPIO-017 a NULL base counts as an empty string");
CHECK(strcmp(canon("/a", NULL), "/a") == 0, "TC-LFTPIO-018 a NULL name counts as an empty string");
CHECK(strcmp(canon(NULL, NULL), "/") == 0, "TC-LFTPIO-019 both NULL -> /");
CHECK(strcmp(canon("/", ".."), "/") == 0, "TC-LFTPIO-020 .. on the root is still the root");
reset_all();
g_fail_at = 1;
CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL,
"TC-LFTPIO-021 the first allocation failing returns NULL");
CHECK(g_alloc_calls == 1, "TC-LFTPIO-021b no second allocation is attempted after that failure");
reset_all();
g_fail_at = 2;
CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL,
"TC-LFTPIO-022 the result buffer allocation failing returns NULL");
CHECK(g_free_calls == 1, "TC-LFTPIO-022b the first block was freed (no heap leak)");
}
/* ===================== 打开文件:守卫 / 卷锁 / 错误通道 ===================== */
static void tc_open(void)
{
int rc;
printf("-- TC-LFTPIO-030..037 open_read / open_write --\n");
reset_all();
rc = lftpd_io_open_read("/a/b.txt");
CHECK((rc == 0) && (g_open_mode == FA_READ) &&
(strcmp(g_open_path, "a/b.txt") == 0),
"TC-LFTPIO-030 success: FA_READ plus the mapped path");
reset_all();
s_file_open = 1;
rc = lftpd_io_open_read("/x");
CHECK((rc == -1) && (g_open_calls == 0) && (g_wait_calls == 0),
"TC-LFTPIO-031 single-file slot guard: while a file is open it touches neither the volume lock nor FatFs");
reset_all();
g_open_rc = FR_NO_FILE;
rc = lftpd_io_open_read("/x");
CHECK((rc == -1) && (g_release_calls == 1) && (g_lfio_error_count == 1),
"TC-LFTPIO-032 open failure: return -1 + log + release the volume lock");
reset_all();
rc = lftpd_io_open_read("/x");
CHECK((rc == 0) && (g_release_calls == 0) && (s_file_open == 1),
"TC-LFTPIO-033 the volume lock stays held after success (exclusive for the whole transfer)");
reset_all();
g_wait_seq[0] = 0; /* 第一轮:卷被 defaultTask 占着 */
g_wait_seq[1] = 0; /* 第二轮:仍未就绪 */
g_wait_seq[2] = 1; /* 第三轮:拿到 */
rc = lftpd_io_open_read("/x");
CHECK((rc == 0) && (g_beat_calls == 2) && (g_wait_calls == 3)
&& (s_last_wait_to == 100u),
"TC-LFTPIO-034 the lock-wait loop feeds the watchdog every round (missing a feed resets the whole device) + every slice asks timeout=100ms (never try, never forever)");
reset_all();
rc = lftpd_io_open_write("/up.bin");
CHECK((rc == 0) && (g_open_mode == (BYTE)(FA_WRITE | FA_CREATE_ALWAYS)),
"TC-LFTPIO-035 open_write uses overwrite mode");
reset_all();
s_file_open = 1;
rc = lftpd_io_open_write("/up.bin");
CHECK((rc == -1) && (g_open_calls == 0), "TC-LFTPIO-036 open_write has the same slot guard");
reset_all();
g_open_rc = FR_DISK_ERR;
rc = lftpd_io_open_write("/up.bin");
CHECK((rc == -1) && (g_release_calls == 1), "TC-LFTPIO-037 an open_write failure also releases the volume lock");
}
/* ===================== trim ===================== */
static void tc_trim(void)
{
char buf[32];
char *r;
printf("-- TC-LFTSTR-001..005 lftpd_string_ltrim --\n");
/* 实现是"返回一个跳过行首空白的指针",**不搬移缓冲**;调用方 lftpd.c:780
* 用的正是返回值。所以断言必须落在返回值上,落在 buf 上是错的。 */
(void)strcpy(buf, " PASS x");
r = lftpd_string_ltrim(buf);
CHECK((r != buf) && (strcmp(r, "PASS x") == 0) && (buf[0] == ' '),
"TC-LFTSTR-001 leading blanks are skipped via the returned pointer, the buffer itself is not moved");
(void)strcpy(buf, "NOOP");
r = lftpd_string_ltrim(buf);
CHECK((r == buf) && (strcmp(buf, "NOOP") == 0), "TC-LFTSTR-002 with no leading blanks the same pointer comes back");
(void)strcpy(buf, " USER admin ");
r = lftpd_string_ltrim(buf);
CHECK(strcmp(r, "USER admin ") == 0, "TC-LFTSTR-003 blanks inside the text are kept");
(void)strcpy(buf, " \t\r\n");
r = lftpd_string_ltrim(buf);
CHECK(r[0] == '\0', "TC-LFTSTR-004 all blanks -> empty string");
/* 现状桩(不是期望行为):尾部循环从 strlen(p) 起步,那里是 '\0',
* isspace(0)=0 立刻退出 —— **尾部空白/CRLF 实际从未被裁**。今天无害是因为
* CRLF 在 lftpd_inet_read_line(lftpd_inet.c 的 strstr("\r\n"))里已截掉。
* 谁哪天把 trim 修对了,这条会红——那是提醒,不是回归。 */
/* 2026-10-23 处置:函数已由 lftpd_string_trim 改名为 lftpd_string_ltrim,
* 并删掉那条永不执行的尾部循环。于是"不去尾部"从潜伏陷阱变成**写进名字与注释
* 的契约**:RFC 959 里参数的尾随空格属于参数本体(PASS 口令、含空格的文件名),
* 去掉等于静默改数据;CRLF 由 lftpd_inet_read_line 截断。 */
(void)strcpy(buf, "LIST ");
r = lftpd_string_ltrim(buf);
CHECK(strcmp(r, "LIST ") == 0,
"TC-LFTSTR-005 trailing spaces must survive (ltrim semantics, they belong to the RFC 959 argument)");
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== LFTPD IO Tests ===\n");
tc_fatfs_path();
tc_canon();
tc_open();
tc_trim();
printf("\n=== LFTPD IO Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}