Files
STM32F4-Base/test/host/lftpd_io/lftpd_io_test.c
T
wandering1 5224f21622 fix(NET,lftpd): 处置覆盖率照出的三处死路径(改名 + 修 timeout=0 + 激活异常集)
三处都由本轮 P1 新增的主机单测照出,用户逐条裁定后实施。共同性质:都不是改动引入
的回归,而是**从没被执行到的分支**——生产路径上要么没有调用者、要么没走边界,所以
编译器(0 错 0 警)、cppcheck、真机日志对它都无话可说。

1) 缺陷一(选 a:改名,行为零变化)
   lftpd_string_trim 的尾部循环起点落在 strlen(p) 那一格 NUL,isspace(0) 为假 →
   循环体一次都不执行,"尾部空白从未被裁",而名字承诺两头都去。改名
   lftpd_string_ltrim + 删掉那条死循环 + 在 .h/.c 写明"只去前导"。不去尾才是对的:
   RFC 959 以命令终止符 CRLF 界定命令,参数(PASS 口令、含空格的文件名)的尾随
   空格属于参数本体,去掉等于静默改数据;CRLF 已由 lftpd_inet_read_line 截断。
   调用点 lftpd.c 同步改名。全库无遗留真引用(余 6 处旧名均为解释改名历史的散文)。

2) 缺陷二(按推荐修)
   net_select 的 timeout = {0,0} 换算出 timeout_ms = 0,而退出条件写着
   if ((timeout_ms > 0) && (已用时 >= timeout_ms)) → 超时永不生效,退化成死等;
   POSIX 语义是"只查一轮立即返回"。去掉 timeout_ms > 0 这个错误前置即可。就绪
   检查在本判断之前,故有就绪时仍立刻带计数返回;同文件 net_poll_events 的
   timeout >= 0 本来就对,现在两条对齐。风险面:拿它当"非阻塞查一遍"会让 netTask
   回不到主循环,而 netTask 在 IWDG required_mask 里(串行 socket 栈唯一执行者)。

3) 缺陷三(选 ①:按 POSIX 置位)
   exceptfds 是恒为空的死参数:循环开头 p_sock == NULL → continue 先跳过,而
   check_socket_error() 只有"表项为空 / fd 越界"才返回 1,越界的 i 又不在本循环
   范围内 → 调用方按 POSIX 直觉用它收异常,永远什么都收不到,且返回码不报错。
   把异常判定提到 continue 之前:表项不存在的 fd 计入 exceptfds 与返回计数,并更新
   函数注释。选 ① 会改变返回计数语义,故补一条"未请求异常集时不计数"的用例,
   确保只查可读的老调用方不被误伤(TC-NETSEL-012b)。

测试同步翻转:原 TC-NETSEL-008 / TC-NETSEL-012 是"现状桩、改了会响",现在改成正向
契约,并新增 TC-NETSEL-012c(异常位只落到被请求的那个集合);TC-LFTSTR-005 从
"尾部不裁"的现状登记改为正向契约(尾随空格必须保留)。net_select 22/22、
lftpd_io 33/33 全绿。

@build:0 错误 0 警告。实测体积 ER_IROM1 232,512 B(快照 232,536 → **−24 B**)、
Flash 233,704 B(−20 B)、RW_IRAM1 75,272 B(+8 B 是上一提交 net_cfg 那个字节,
本批零新增)、**RW_CCM 57,376 B 不变**;四档百分比 22.2/57.4/87.9/22.3 全不变。
Code 减少 36 B 即删掉的死循环。

覆盖率台账:门禁如实报了一次 lftpd_string.c 已覆盖行 8 → 7(**代码少了一行**,不是
覆盖退化),已 --update-baseline 重登;现 22/22 套件、line=3003/3446(87.1%)、
func=236/259(91.1%)、tested_files=26、untested_files=118。

文档:主机单测覆盖率口径 §9 新增"三处死路径的处置"小节(裁定、改法、影响、钉住
它的用例一一对应),并把两处"等裁定"的登记改为已处置。

自查:coding-standard / test-cppcheck / id-check --strict(无台账外新账)/ doc-links
(35 处 = 存量)全过;八个改动文件与 HEAD 逐项比对 BOM/CR/U+FFFD/行尾无变化
(这几个文件本就不带 BOM,未被动过)。pre-commit --all-files 仍只有三条失败且与
本分支无关:mem-numbers / mem-budget(.map 与 docs/内存管理 受管快照漂移,增量见
上,按并行分支约定不在分支里 --write,等 Lead 合并后统一回填)与 ui-golden
(simulator/tests/ref/*.bin 在 .gitignore 内,新建 worktree 天生没有黄金参考)。
2026-10-23 23:44:48 +08:00

432 lines
17 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;
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/空串 -> 0:");
CHECK(strcmp(to_fatfs_path("/"), "0:") == 0, "TC-LFTPIO-002 根 -> 0:");
CHECK(strcmp(to_fatfs_path("/a/b.txt"), "a/b.txt") == 0,
"TC-LFTPIO-003 去掉前导斜杠");
CHECK(strcmp(to_fatfs_path("a.txt"), "a.txt") == 0, "TC-LFTPIO-004 原样返回");
}
/* ===================== 路径规范化 ===================== */
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 相对拼接");
CHECK(strcmp(canon("/dir", "/abs.txt"), "/abs.txt") == 0, "TC-LFTPIO-011 绝对忽略 base");
CHECK(strcmp(canon("/a/b", "../c"), "/a/c") == 0, "TC-LFTPIO-012 上跳一级");
CHECK(strcmp(canon("/a", "../../x"), "/x") == 0,
"TC-LFTPIO-013 上跳过头仍被钳在根内(安全属性)");
CHECK(strcmp(canon("/a", "."), "/a") == 0, "TC-LFTPIO-014 丢弃 . 段");
CHECK(strcmp(canon("/a", ".."), "/") == 0, "TC-LFTPIO-015 回退到根");
CHECK(strcmp(canon("/a//", "b"), "/a/b") == 0, "TC-LFTPIO-016 段间多余斜杠被折叠");
CHECK(strcmp(canon("/dir", "//x"), "/x") == 0,
"TC-LFTPIO-016b name 以斜杠开头按**绝对路径**处理(忽略 base,仍钳在根内)");
CHECK(strcmp(canon(NULL, "x"), "/x") == 0, "TC-LFTPIO-017 base NULL 视作空串");
CHECK(strcmp(canon("/a", NULL), "/a") == 0, "TC-LFTPIO-018 name NULL 视作空串");
CHECK(strcmp(canon(NULL, NULL), "/") == 0, "TC-LFTPIO-019 两个 NULL -> /");
CHECK(strcmp(canon("/", ".."), "/") == 0, "TC-LFTPIO-020 根上加 .. 仍是根");
reset_all();
g_fail_at = 1;
CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL,
"TC-LFTPIO-021 首块分配失败返回 NULL");
CHECK(g_alloc_calls == 1, "TC-LFTPIO-021b 失败后不再分配第二块");
reset_all();
g_fail_at = 2;
CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL,
"TC-LFTPIO-022 结果缓冲分配失败返回 NULL");
CHECK(g_free_calls == 1, "TC-LFTPIO-022b 第一块已释放(不泄漏堆)");
}
/* ===================== 打开文件:守卫 / 卷锁 / 错误通道 ===================== */
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 成功:FA_READ + 映射后的路径");
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 单文件槽守卫:已打开时不碰卷锁与 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 打开失败:返回 -1 + 留痕 + 释放卷锁");
reset_all();
rc = lftpd_io_open_read("/x");
CHECK((rc == 0) && (g_release_calls == 0) && (s_file_open == 1),
"TC-LFTPIO-033 成功后卷锁不释放(传输期独占)");
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 等锁循环每轮喂看门狗(漏喂 = 整机复位)");
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 用覆盖写模式");
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 同样有槽守卫");
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 open_write 失败也释放卷锁");
}
/* ===================== 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 行首空白靠返回指针跳过,原缓冲不搬移");
(void)strcpy(buf, "NOOP");
r = lftpd_string_ltrim(buf);
CHECK((r == buf) && (strcmp(buf, "NOOP") == 0), "TC-LFTSTR-002 无前导空白返回同一指针");
(void)strcpy(buf, " USER admin ");
r = lftpd_string_ltrim(buf);
CHECK(strcmp(r, "USER admin ") == 0, "TC-LFTSTR-003 词中间空白保留");
(void)strcpy(buf, " \t\r\n");
r = lftpd_string_ltrim(buf);
CHECK(r[0] == '\0', "TC-LFTSTR-004 全空白 -> 空串");
/* 现状桩(不是期望行为):尾部循环从 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 尾随空格必须保留(ltrim 语义,RFC 959 参数本体)");
}
/* ===================== 入口 ===================== */
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;
}