b623a406ad
这四笔在主树里已经互相纠缠(同一次 @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 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
437 lines
18 KiB
C
437 lines
18 KiB
C
/*
|
||
* 模块名称: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;
|
||
}
|