Files
STM32F4-Base/test/host/lftpd/lftpd_test.c
T
wandering1 924ac3d428 test(host): 断言字面量统一纯 ASCII,30 文件 1535 行
用户裁定「统一」。口径就是 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 回填留待主工作树合并后。
2026-10-24 21:00:17 +08:00

737 lines
26 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 控制通道协议层(lftpd)主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/NET/lftpd/lftpd.c 的控制通道状态机与 17 条
* 命令处理器:认证前置白名单、超长命令名拒绝(不越界写命令缓冲)、未知命令
* 502、QUIT 收尾时释放数据监听、大小写不敏感、PASV/EPSV 的 227/229 响应
* 编码、数据 accept 失败时不残留半开监听、STOR 失败要**删掉半成品**、
* 写开关关闭时 DELE/STOR 一律 550、CWD 的目录判定与截断安全、FEAT 多行
* 响应的首尾行格式。
*
* 被测对象:真实源文件 ../../../../Drivers/BSP/NET/lftpd/lftpd.c(#include 纳入,
* 因此命令表 commands[]、24 个 static cmd_* 与 handle_control_channel 都
* 可在同一 TU 内直接驱动)。假的是它下面三层:inet 适配(脚本化命令行
* 队列 + 响应记录)、io 层(极简内存视图:stat/open/read/write/opendir/
* readdir/unlink + 可注入失败)、认证与配置(ftp_auth_check/ftp_cfg_get
* 可注入结果,认证本身有 test/host/ftp_auth 的 10 条专测,不重复)。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. **半成品文件**是 FTP 最贵的坑:STOR 传到一半对端断了,如果不 unlink,Flash 上
* 就永久留一个残缺文件(现场表现为"升级包传坏了再也传不上去")。真机上要卡准
* "传一半掐线"的时机,主机侧把 io 层的写失败注进去就行。
* 2. 数据通道是**单连接 socket**:LIST/RETR/STOR 每条命令用完必须把监听关掉并复位
* data_socket=-1,否则下一条命令拿着上一个号继续 accept,表现是偶发连不上且
* 与上一会话时序相关。这类"忘了复位"只有把 accept 失败也注进来才测得干净。
* 3. 命令名缓冲是 command_tmp[4+1],而判断用 if (index >= 5) 先拦——这条边界
* (恰好 5 个字符的命令名必须被拒)就是防 memcpy 越界,改一行就没了。
* 4. 写开关([ftp] write_en=0)是现场防误删的闸门,DELE/STOR 必须在**碰文件之前**
* 就拒绝,且此时不该去 accept 数据通道。
*
* 覆盖点:TC-LFTPD-001 认证白名单;002 超长命令名;003 未知命令 502;004 QUIT 退出;
* 005 欢迎未送达就收工;006 退出时释放数据监听;007 大小写不敏感;008 无参数命令;
* 010 未开被动模式的数据命令 425;011/012/013 PASV/EPSV 响应;014 数据 accept 失败
* 不留残口;015/016 LIST 成功与目录打不开;017/018 RETR 成功与打不开;
* 019/020 STOR 删半成品与写开关;021 DELE 写开关;022/023 CWD;024 SIZE;025 PWD;
* 026 SYST;027 FEAT 多行;028/029 口令失败与锁定;030 TYPE/NOOP。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lftpd.h"
#include "lftpd_auth.h"
#include "ftp_cfg.h"
#include "ff.h"
#include "lftpd.c"
/* 命令行 trim 用**真实实现**(16 行、零依赖;lftpd_string.c 的契约由
test/host/lftpd_io 的 TC-LFTSTR-* 专测,这里只是满足链接、顺带让参数解析走真码)。 */
#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)
/* ===================== 桩:日志计数 =====================
* lftpd_log_info/error/peer/debug 是真实 private/lftpd_log.h 里的**宏**,展开成
* DBG_*;本模块只桩最底下的 dbg_log.h(计数)。这样真实分级逻辑一起被编译验证:
* peer 级归 INFO、debug 级在 MCU 上被整条摘掉。 */
int g_lg_info_count;
int g_lg_error_count;
int g_lg_debug_count;
/* ===================== 桩:假堆(返回给 vPortFree 的路径缓冲) ===================== */
static uint8_t g_pool[2048];
static size_t g_pool_used;
static int g_free_calls;
void *pvPortMalloc(size_t xWantedSize)
{
void *p;
g_pool_used = (g_pool_used + 7u) & ~(size_t)7u;
if ((g_pool_used + xWantedSize) > sizeof(g_pool)) {
return NULL;
}
p = &g_pool[g_pool_used];
g_pool_used += xWantedSize;
return p;
}
void vPortFree(void *pv)
{
if (pv != NULL) {
g_free_calls++;
}
}
/* ===================== 假 inet 层:脚本化命令行 + 响应记录 ===================== */
typedef struct {
const char *lines[16];
int line_ret[16];
int line_n;
int line_i;
char replies[32][256];
int reply_n;
int string_rc; /* write_string 返回(0=成功) */
int listen_rc;
int port;
int accept_to_rc; /* 数据通道 accept 结果 */
int data_write_rc; /* 数据写返回(<0 模拟对端断) */
int rx_left; /* 数据通道还剩多少字节可读 */
int close_calls;
int listen_calls;
int accept_calls;
int delay_calls;
} lftp_net_t;
static lftp_net_t g_n;
int lftpd_inet_read_line(int socket, char *buffer, size_t buffer_len)
{
(void)socket;
if (g_n.line_i >= g_n.line_n) {
return -1; /* 脚本喂完 = 对端关闭 */
}
(void)strncpy(buffer, g_n.lines[g_n.line_i], buffer_len - 1u);
buffer[buffer_len - 1u] = '\0';
return g_n.line_ret[g_n.line_i++];
}
int lftpd_inet_write_string(int socket, const char *message)
{
(void)socket;
if (g_n.reply_n < (int)(sizeof(g_n.replies) / sizeof(g_n.replies[0]))) {
(void)strncpy(g_n.replies[g_n.reply_n], message, sizeof(g_n.replies[0]) - 1u);
g_n.replies[g_n.reply_n][sizeof(g_n.replies[0]) - 1u] = '\0';
g_n.reply_n++;
}
return g_n.string_rc;
}
int lftpd_inet_write(int socket, const void *buf, int len)
{
(void)socket;
(void)buf;
return (g_n.data_write_rc != 0) ? g_n.data_write_rc : len;
}
int lftpd_inet_read(int socket, void *buf, int len)
{
int n;
(void)socket;
(void)buf;
if (g_n.rx_left <= 0) {
return 0; /* 对端结束数据 */
}
n = (g_n.rx_left > len) ? len : g_n.rx_left;
g_n.rx_left -= n;
return n;
}
int lftpd_inet_listen(int port)
{
(void)port;
g_n.listen_calls++;
return g_n.listen_rc;
}
int lftpd_inet_get_socket_port(int socket)
{
(void)socket;
return g_n.port;
}
int lftpd_inet_accept(int socket)
{
(void)socket;
return -1;
}
int lftpd_inet_accept_timeout(int socket, int timeout_ms)
{
(void)socket;
(void)timeout_ms;
g_n.accept_calls++;
return g_n.accept_to_rc;
}
int lftpd_inet_close(int socket)
{
(void)socket;
g_n.close_calls++;
return 0;
}
/* ===================== 假 io 层:极简内存视图 ===================== */
typedef struct {
int stat_rc;
int is_dir;
uint32_t size;
int open_read_rc;
int open_write_rc;
int write_short; /* >0:写返回这个短值 -> 触发半成品清理 */
int read_left;
int opendir_rc;
const char *names[4];
uint32_t sizes[4];
int dirs[4];
int readdir_i;
int readdir_n;
int unlink_calls;
char last_path[128];
} lftp_io_t;
static lftp_io_t g_io;
char *lftpd_io_canonicalize_path(const char *base, const char *name)
{
char *p = (char *)pvPortMalloc(128);
if (p == NULL) {
return NULL;
}
(void)snprintf(p, 128, "%s/%s", (base != NULL) ? base : "",
(name != NULL) ? name : "");
(void)strncpy(g_io.last_path, p, sizeof(g_io.last_path) - 1u);
return p;
}
int lftpd_io_stat(const char *path, uint32_t *p_size, int *p_is_dir)
{
(void)path;
if (p_size != NULL) { *p_size = g_io.size; }
if (p_is_dir != NULL) { *p_is_dir = g_io.is_dir; }
return g_io.stat_rc;
}
int lftpd_io_open_read(const char *path) { (void)path; return g_io.open_read_rc; }
int lftpd_io_open_write(const char *path) { (void)path; return g_io.open_write_rc; }
int lftpd_io_read(int handle, void *buf, int len)
{
int n;
(void)handle;
(void)buf;
if (g_io.read_left <= 0) {
return 0;
}
n = (g_io.read_left > len) ? len : g_io.read_left;
g_io.read_left -= n;
return n;
}
int lftpd_io_write(int handle, const void *buf, int len)
{
(void)handle;
(void)buf;
return (g_io.write_short > 0) ? g_io.write_short : len;
}
void lftpd_io_close(int handle) { (void)handle; }
int lftpd_io_unlink(const char *path) { (void)path; g_io.unlink_calls++; return 0; }
void *lftpd_io_opendir(const char *path)
{
(void)path;
return (g_io.opendir_rc != 0) ? NULL : (void *)&g_io;
}
int lftpd_io_readdir(void *dp, char *name, int name_max, uint32_t *p_size, int *p_is_dir)
{
int i = g_io.readdir_i;
(void)dp;
if (i >= g_io.readdir_n) {
return -1;
}
g_io.readdir_i++;
(void)strncpy(name, g_io.names[i], (size_t)name_max - 1);
name[name_max - 1] = '\0';
*p_size = g_io.sizes[i];
*p_is_dir = g_io.dirs[i];
return 0;
}
void lftpd_io_closedir(void *dp) { (void)dp; }
/* ===================== 假配置与假认证 ===================== */
static ftp_cfg_t g_cfg;
static int g_cfg_present = 1;
static int g_default_pwd;
static ftp_auth_result_t g_auth_res;
static int g_auth_calls;
const ftp_cfg_t *ftp_cfg_get(void) { return (g_cfg_present != 0) ? &g_cfg : NULL; }
int ftp_cfg_is_default_pwd(void) { return g_default_pwd; }
ftp_auth_result_t ftp_auth_check(const ftp_auth_creds_t *p_creds, uint8_t user_ok,
const char *p_pwd, ftp_auth_state_t *p_state,
uint32_t now_ms)
{
(void)p_creds;
(void)user_ok;
(void)p_pwd;
(void)p_state;
(void)now_ms;
g_auth_calls++;
return g_auth_res;
}
int ftp_auth_user_match(const char *p_cfg_user, const char *p_given)
{
if ((p_cfg_user == NULL) || (p_given == NULL)) {
return 0;
}
return (strcmp(p_cfg_user, p_given) == 0) ? 1 : 0;
}
/* ===================== 假 net 与 OS ===================== */
static uint32_t g_tick;
static int g_sockname_rc; /* net_getsockname 的返回码(PASV 分支注入失败用) */
uint32_t xTaskGetTickCount(void) { return g_tick; }
/* lftpd_task_wd_beat 的真实定义在 lftpd_inet.c 里(__weak,供 lftpd.c 覆盖),本模块
不拖那一层进来,故在此给计数假件——正好把"传输循环每轮喂狗"变成可断言的事。 */
static int g_wd_beats;
void lftpd_task_wd_beat(void) { g_wd_beats++; }
void vTaskDelay(TickType_t t)
{
(void)t;
g_n.delay_calls++;
if (g_n.delay_calls > 200) {
printf(" ABORT loop runaway (>200 rounds)\n");
fflush(stdout);
exit(2);
}
}
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen)
{
struct net_sockaddr_in *a = (struct net_sockaddr_in *)addr;
(void)sockfd;
(void)addrlen;
a->sin_addr.s_addr = 0x0101A8C0u; /* 192.168.1.1 的宿主序表示(假件自定) */
a->sin_port = net_htons(21u);
return g_sockname_rc;
}
int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen)
{
(void)sockfd;
(void)addr;
(void)addrlen;
return 0;
}
uint16_t net_htons(uint16_t v) { return (uint16_t)((v >> 8) | (v << 8)); }
uint16_t net_ntohs(uint16_t v) { return net_htons(v); }
uint32_t net_htonl(uint32_t v)
{
return ((v & 0xFFu) << 24) | (((v >> 8) & 0xFFu) << 16) |
(((v >> 16) & 0xFFu) << 8) | ((v >> 24) & 0xFFu);
}
char *net_inet_ntoa(uint32_t in, char *p_buf)
{
(void)snprintf(p_buf, 16, "%u.%u.%u.%u", (unsigned)(in & 0xFFu),
(unsigned)((in >> 8) & 0xFFu), (unsigned)((in >> 16) & 0xFFu),
(unsigned)((in >> 24) & 0xFFu));
return p_buf;
}
/* ===================== 复位与驱动 ===================== */
static lftpd_client_t g_cli;
static void reset_all(void)
{
(void)memset(&g_n, 0, sizeof(g_n));
(void)memset(&g_io, 0, sizeof(g_io));
g_n.string_rc = 0;
g_n.listen_rc = 20;
g_n.port = 5000;
g_n.accept_to_rc = 21; /* 默认数据通道能连上 */
g_n.data_write_rc = 0;
g_io.stat_rc = 0;
g_io.is_dir = 1;
g_io.size = 7u;
g_io.open_read_rc = 0;
g_io.open_write_rc = 0;
g_io.read_left = 0;
g_io.readdir_n = 0;
g_cfg_present = 1;
(void)memset(&g_cfg, 0, sizeof(g_cfg));
(void)strcpy(g_cfg.user, "admin");
g_cfg.write_en = 1u;
g_auth_res = FTP_AUTH_OK;
g_default_pwd = 0;
g_tick = 0u;
g_wd_beats = 0;
g_free_calls = 0;
g_pool_used = 0u;
g_lg_info_count = 0;
g_lg_error_count = 0;
g_lg_debug_count = 0;
g_sockname_rc = 0;
(void)memset(&g_cli, 0, sizeof(g_cli));
(void)strcpy(g_cli.directory, "/");
g_cli.socket = 10;
g_cli.data_socket = -1;
/* 不写死字段名:认证状态的结构由 lftpd_auth.h 定义,本模块只把它当黑盒清零
(口令/锁定逻辑有 test/host/ftp_auth 的 10 条专测,这里不重复验)。 */
(void)memset(&s_auth_state, 0, sizeof(s_auth_state));
}
static void feed(const char *l0, const char *l1, const char *l2, const char *l3)
{
int n = 0;
g_n.lines[n] = l0; g_n.line_ret[n++] = 0;
if (l1 != NULL) { g_n.lines[n] = l1; g_n.line_ret[n++] = 0; }
if (l2 != NULL) { g_n.lines[n] = l2; g_n.line_ret[n++] = 0; }
if (l3 != NULL) { g_n.lines[n] = l3; g_n.line_ret[n++] = 0; }
g_n.line_n = n;
}
static const char *rep(int k) { return (k < g_n.reply_n) ? g_n.replies[k] : ""; }
static int rep_has(const char *prefix)
{
int i;
int c = 0;
for (i = 0; i < g_n.reply_n; i++) {
if (strncmp(g_n.replies[i], prefix, strlen(prefix)) == 0) {
c++;
}
}
return c;
}
/* ===================== 控制通道闸门 ===================== */
static void tc_gates(void)
{
printf("-- TC-LFTPD-001..008 control channel gates --\n");
reset_all();
feed("RETR a.txt", "LIST", "PWD", NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("220 ") == 1) && (rep_has("530 ") == 3) &&
(g_n.accept_calls == 0),
"TC-LFTPD-001 before login only USER/PASS/QUIT/FEAT/SYST/NOOP pass, everything else gets 530");
reset_all();
feed("ABCDEFG", "NOOP", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("500 ") == 1) && (rep_has("200 ") == 1),
"TC-LFTPD-002 a command name of 5 characters or more is rejected first (command_tmp has only 5 slots, this keeps memcpy in bounds)");
reset_all();
g_cli.auth_logged_in = 1; /* 否则会被认证白名单先以 530 拦掉,测不到分派 */
feed("FOO bar", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("502 ") == 1) && (rep_has("530 ") == 0),
"TC-LFTPD-003 an unknown command answers 502 instead of dropping the connection");
reset_all();
feed("QUIT", "NOOP", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("221 ") == 1) && (rep_has("200 ") == 0) && (g_n.line_i == 1),
"TC-LFTPD-004 after QUIT no further command is read (the session ends here)");
reset_all();
g_n.string_rc = -1; /* 220 都送不出去 */
feed("NOOP", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((g_n.line_i == 0) && (g_lg_info_count >= 1),
"TC-LFTPD-005 if the welcome never got through (peer already gone) finish straight away instead of spinning on reads; that trace really is INFO");
reset_all();
g_cli.data_socket = 33; /* 被动模式已开但客户端直接跑了 */
feed("QUIT", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((g_n.close_calls == 1) && (g_cli.data_socket == -1),
"TC-LFTPD-006 on exit the still-open data listener must be released (a single-socket chip must not be left with a dangling socket)");
reset_all();
g_cli.auth_logged_in = 1;
feed("noop", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK(rep_has("200 ") == 1, "TC-LFTPD-007 command names are case insensitive");
reset_all();
g_cli.auth_logged_in = 1;
g_io.opendir_rc = 0;
g_io.readdir_n = 0;
g_n.listen_rc = 20;
feed("LIST", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("425 ") == 1) && (g_n.accept_calls == 0),
"TC-LFTPD-010 a data command without passive mode set first gets 425 and never reaches accept");
}
/* ===================== 被动模式与数据通道 ===================== */
static void tc_passive(void)
{
printf("-- TC-LFTPD-011..016 passive & data --\n");
reset_all();
g_cli.auth_logged_in = 1;
/* 直调处理器:走 handle_control_channel 的话,会话退出时 cleanup 一定把被动监听
关掉(那正是 TC-LFTPD-006 要验的行为),这里要看的是 PASV 自己留下的状态。 */
(void)cmd_pasv(&g_cli, NULL);
CHECK((strcmp(rep(0), "227 Entering Passive Mode (192,168,1,1,19,136).\r\n") == 0) &&
(g_cli.data_socket == 20) && (g_n.listen_calls == 1),
"TC-LFTPD-011 the 227 tuple: ip through htonl then high byte to low byte (dotted order), port high/low, and the passive listen handle is recorded");
reset_all();
g_cli.auth_logged_in = 1;
g_sockname_rc = -1; /* 问不到本机地址 */
feed("PASV", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("425 ") == 1) && (g_n.close_calls == 1) &&
(g_cli.data_socket == -1),
"TC-LFTPD-012 when the local ip is unavailable the listener just created must be closed (no half-passive mode left behind)");
reset_all();
g_cli.auth_logged_in = 1;
feed("EPSV", NULL, NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK(strcmp(rep(1), "229 Entering Extended Passive Mode (|||5000|).\r\n") == 0,
"TC-LFTPD-013 EPSV reports only the port (an IPv6 client reuses the control connection address)");
reset_all();
g_cli.auth_logged_in = 1;
g_n.listen_rc = 20;
g_n.accept_to_rc = -1; /* 对端一直不开数据通道 */
feed("PASV", "LIST", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("150 ") == 1) && (rep_has("425 ") == 1) &&
(g_cli.data_socket == -1) && (g_n.close_calls == 1),
"TC-LFTPD-014 a failed data accept reports 425 and closes and resets the listener instead of leaving it to the next command");
reset_all();
g_cli.auth_logged_in = 1;
g_io.readdir_n = 2;
g_io.names[0] = "a.txt"; g_io.sizes[0] = 7u; g_io.dirs[0] = 0;
g_io.names[1] = "sub"; g_io.sizes[1] = 0u; g_io.dirs[1] = 1;
feed("PASV", "LIST", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("226 ") == 1) && (g_n.data_write_rc == 0),
"TC-LFTPD-015 LIST success: 150 first, the directory goes out on the data socket, then 226 closes it");
reset_all();
g_cli.auth_logged_in = 1;
g_io.opendir_rc = 1; /* 目录打不开 */
feed("PASV", "LIST", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK(rep_has("550 ") == 1, "TC-LFTPD-016 when the directory cannot be opened LIST answers 550 (not 226)");
}
/* ===================== 传输与写开关 ===================== */
static void tc_transfer(void)
{
printf("-- TC-LFTPD-017..021 transfer & write gate --\n");
reset_all();
g_cli.auth_logged_in = 1;
g_io.read_left = 5;
feed("PASV", "RETR a.txt", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("226 ") == 1) && (g_lg_info_count >= 1) && (g_wd_beats >= 1),
"TC-LFTPD-017 RETR success: 226 at the end, a DOWNLOAD record is kept and the transfer loop feeds the watchdog");
reset_all();
g_cli.auth_logged_in = 1;
g_io.open_read_rc = -1;
feed("PASV", "RETR gone.txt", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("450 ") == 1) && (g_lg_error_count >= 1),
"TC-LFTPD-018 a file that cannot be opened answers 450 (a transfer failure and an invalid command must be reported apart)");
reset_all();
g_cli.auth_logged_in = 1;
g_n.rx_left = 900;
g_io.write_short = 100; /* 写到一半失败 */
feed("PASV", "STOR up.bin", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((g_io.unlink_calls == 1) && (rep_has("450 ") == 1),
"TC-LFTPD-019 a STOR failing halfway must delete the partial file (otherwise a broken file stays on flash forever)");
reset_all();
g_cli.auth_logged_in = 1;
g_cfg.write_en = 0u;
g_n.listen_rc = 20;
feed("PASV", "STOR up.bin", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("550 ") == 1) && (g_n.accept_calls == 0) &&
(g_io.open_write_rc == 0) && (g_io.unlink_calls == 0),
"TC-LFTPD-020 with writes disabled STOR is refused before the file is touched and no data channel is opened");
reset_all();
g_cli.auth_logged_in = 1;
g_cfg.write_en = 0u;
(void)cmd_dele(&g_cli, "a.txt");
CHECK(rep_has("550 ") == 1, "TC-LFTPD-021 the write switch blocks DELE as well");
reset_all();
g_cli.auth_logged_in = 1;
g_io.is_dir = 1;
(void)cmd_dele(&g_cli, "sub");
CHECK((rep_has("550 ") == 1) && (g_io.unlink_calls == 0),
"TC-LFTPD-022 DELE must not delete directories (this implementation only deletes files)");
reset_all();
g_cli.auth_logged_in = 1;
g_io.is_dir = 0;
(void)cmd_dele(&g_cli, "a.txt");
CHECK((g_io.unlink_calls == 1) && (rep_has("250 ") == 1),
"TC-LFTPD-022b deleting a regular file goes through unlink and answers 250");
}
/* ===================== 目录与元信息 ===================== */
static void tc_meta(void)
{
printf("-- TC-LFTPD-023..030 cwd / size / pwd / feat --\n");
reset_all();
(void)cmd_cwd(&g_cli, "");
CHECK((rep_has("550 ") == 1) && (strcmp(g_cli.directory, "/") == 0),
"TC-LFTPD-023 CWD with an empty argument is refused and the current directory stays");
reset_all();
g_io.is_dir = 0;
(void)cmd_cwd(&g_cli, "a.txt");
CHECK(rep_has("550 ") == 1, "TC-LFTPD-023b CWD onto a regular file is refused (is_dir must really be a directory)");
reset_all();
g_io.is_dir = 1;
(void)cmd_cwd(&g_cli, "logs");
CHECK((rep_has("250 ") == 1) && (strcmp(g_cli.directory, "//logs") == 0) &&
(g_free_calls == 1),
"TC-LFTPD-024 a successful CWD updates the current directory and frees the path buffer (no heap leak)");
reset_all();
(void)cmd_cwd(&g_cli, "/");
CHECK((rep_has("250 ") == 1) && (g_io.stat_rc == 0),
"TC-LFTPD-024b going back to the root skips stat and succeeds directly (the root always exists)");
reset_all();
g_io.is_dir = 0;
g_io.size = 12345u;
(void)cmd_size(&g_cli, "up.bin");
CHECK(strcmp(rep(0), "213 12345\r\n") == 0, "TC-LFTPD-025 SIZE reports the byte count");
reset_all();
g_io.stat_rc = -1;
(void)cmd_size(&g_cli, "nope");
CHECK(rep_has("550 ") == 1, "TC-LFTPD-025b when the file is missing SIZE answers 550");
reset_all();
(void)strcpy(g_cli.directory, "/logs");
(void)cmd_pwd(&g_cli, NULL);
CHECK(strcmp(rep(0), "257 \"/logs\"\r\n") == 0,
"TC-LFTPD-026 PWD wraps the directory in double quotes (required by RFC 959)");
reset_all();
(void)cmd_syst(&g_cli, NULL);
CHECK(strcmp(rep(0), "215 UNIX Type: L8\r\n") == 0,
"TC-LFTPD-027 SYST always reports UNIX Type: L8 (clients pick their mode from it)");
reset_all();
(void)cmd_feat(&g_cli, NULL);
CHECK((strncmp(rep(0), "211-", 4) == 0) && (strcmp(rep(1), "EPSV\r\n") == 0) &&
(strcmp(rep(2), "PASV\r\n") == 0) && (strncmp(rep(5), "211 ", 4) == 0) &&
(g_n.reply_n == 6),
"TC-LFTPD-028 FEAT multiline: first line 211-, four capability lines without a status code, last line 211 ends it");
reset_all();
(void)cmd_type(&g_cli, "I");
(void)cmd_noop(&g_cli, NULL);
CHECK(rep_has("200 ") == 2, "TC-LFTPD-030 both TYPE and NOOP answer 200");
}
/* ===================== 登录序列 ===================== */
static void tc_login(void)
{
printf("-- TC-LFTPD-031..033 login --\n");
reset_all();
feed("USER admin", "PASS good", "RETR a.txt", NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("331 ") == 1) && (rep_has("230 ") == 1) &&
(g_cli.auth_logged_in == 1u) && (strcmp(g_cli.auth_user, "admin") == 0),
"TC-LFTPD-031 USER records the user name and the match bit; a passing PASS switches to logged in");
reset_all();
g_auth_res = FTP_AUTH_FAIL;
feed("USER admin", "PASS bad", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("530 ") == 1) && (g_cli.auth_logged_in == 0u) &&
(g_lg_info_count >= 1),
"TC-LFTPD-032 a wrong password gives 530 and USER must be repeated (auth_user_ok was cleared)");
reset_all();
g_auth_res = FTP_AUTH_LOCKED;
g_cfg_present = 1;
feed("USER admin", "PASS bad", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK(rep_has("530 ") == 1,
"TC-LFTPD-033 while locked out after repeated failures it also answers only 530 (no distinguishing hint for an attacker)");
reset_all();
g_cfg_present = 0; /* 配置读不到 */
feed("USER admin", "PASS x", NULL, NULL);
(void)handle_control_channel(&g_cli);
CHECK((rep_has("331 ") == 1) && (rep_has("530 ") == 1) &&
(g_cli.auth_user_ok == 0u),
"TC-LFTPD-034 with missing configuration USER cannot match and PASS answers 530 straight away (fail-closed)");
}
int main(void)
{
printf("=== LFTPD Protocol Tests ===\n");
tc_gates();
tc_passive();
tc_transfer();
tc_meta();
tc_login();
printf("\n=== LFTPD Protocol Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}