924ac3d428
用户裁定「统一」。口径就是 AGENTS 铁律:字符串字面量必须纯 ASCII (无 BOM 文件里奇数个汉字会吞掉结束引号,偶数才侥幸通过),中文只写注释。 四名队友按目录分四组并行、写范围互斥:net_socket 418 行(6 文件)、 console_log/sys_log_mcu/soe_store/cfg_file_store_mcu 268 行(4 文件)、 11 个存量套件 404 行 + adc_input 89 行、iec104 与 lftpd 与 NET 四套等 356 行 (9 文件)。runner 的 .py 文档串不在口径内(Python 源不经 ARMCC 代码页)。 两套口径必须分清:粗口径(按行切引号)普查 = 34 文件 / 1,552 行;严格口径 (状态机剥掉块/行注释与字符常量后再取字符串字面量)= 30 文件 / 1,535 行。 差值那 4 个文件的命中全在注释里(ns_peers.c、net_socket/stub/ns_rtos.h、 common/ch395f_fake.h、common/fatfs_fake.c)——按口径一处都不该改,改了等于 把中文从注释里搬走、正好违背口径。粗口径绝不能当交付判据,否则要么白改 注释、要么留下「这行为什么没改」的悬案。 Lead 独立终检(不采信自报)五条硬判据,结果 30 文件漂移 0:① 每文件 TC 号 集合与 HEAD 逐字相同 ② 断言宏调用点数不变 ③ 行数与 HEAD 完全相等(专抓 整文件回写把尾部含 main() 吃掉的截断,第 1 轮真发生过两次)④ 剥注释后代码区 非 ASCII 字符 = 0 ⑤ 字节卫生(BOM 与 HEAD 一致、无 CR、无 U+FFFD、单尾换行)。 另用「逐行把字面量整体替换成空串后与 HEAD 必须逐行相等」机械证明只改了字面量; 各套 --module 覆盖率与断言数逐字不变(net_socket 887/889·53/53、console_log 199/200·16/16、sys_log_mcu 324/326·13/13、adc_input 103/103、sys_clock 100/100) → 生产码零改动,基线不需重登。 闸拦下一处真实回归(我的验收器没抓到):ns_s6_edges.c 翻译时把一个上一轮 已整改成平实描述的编号又写回了 CHECK 标签。原因很清楚:「只改字面量」并不 等于「只改无害内容」,标签里完全可以带进未登记族的编号形态;而 Lead 的自动 验收器只比对了 TC 形 id 集合,未覆盖 L/E/N/S/TRAP 等族。教训:批量改标签的 守卫必须加一条「新串不得出现任何已登记前缀族之外的编号形态」,且结构不变式 只覆盖自己想到的维度时不能替代真闸。 Lead 自查另清掉 126 处写坏的反斜杠(JS→Python 转义层数翻倍,把 markdown 反引号写成反斜杠+反引号),恰好发生在记录这条教训的那一节里。 文档:docs/测试说明/主机单测覆盖率口径.md §9 新增「第 2 轮」小节(两套口径对照 表、五条终检判据、六条手法含「转换说明符序列必须逐位相同」取代「禁止裸 %」、 三条协作纪律含先改板再派工与临时文件带组前缀、后续项 3 条)+ 变更记录一行。 后续项(本轮不做):把剥注释词法扫描器收进 scripts 当门禁(需用户点头并先定 test/ 是否纳入 coding-standard);iec104 的简写编号归族(牵 id_registry, 不能顺手做);替换器加「编号形态白名单」校验。 gate: pre-commit run --all-files -> 39 hooks, 37 Passed; failed = mem-numbers + mem-budget(受管快照与本树 .map 的漂移,属第 1 轮生产码改动的遗留,非本批 增量)。本批只碰 test/host/**,不进 Keil 编译面也不进模拟器 CMake,**RAM/Flash 增量 = 0**;实测水位 RW_IRAM1 57.4%、RW_CCM 87.9%,均低于 92% 红线。 id-check 0 台账外新账(可判定 84.63% > 基线 82.6%);test-coverage 33/33 全绿; cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed。 按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
432 lines
18 KiB
C
432 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;
|
||
|
||
int vol_lock_wait(uint32_t timeout_ms)
|
||
{
|
||
int r;
|
||
|
||
(void)timeout_ms;
|
||
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;
|
||
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),
|
||
"TC-LFTPIO-034 the lock-wait loop feeds the watchdog every round (missing a feed resets the whole device)");
|
||
|
||
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;
|
||
}
|