Files
STM32F4-Base/test/host/lftpd/lftpd_test.c
T
wandering1 7efa58d9a1
CI / 静态门禁 (pre-commit) (push) Failing after 25s
CI / 主机侧单测 (MSVC) (push) Successful in 48s
network: 台架 IP 迁到 192.168.31.100,并把重复写死的值收进单一真源
分两段(用户裁定 2026-11-02):先做地址迁移,再把同类字面量收敛成
单一真源,以后再改地址只动一处。

一、IP 迁移 192.168.1.100 → 192.168.31.100(gw 同步 → .31.1)
- 工厂默认:Drivers/BSP/NETCFG/net_cfg.c 的 NET_CFG_DEF_IP/_GW
- 注释/文档示例、主机与板级测试、台架 py 脚本、两份 sim_config.ini
- 字节序教学点同步换算 hex:0x6401A8C0 → 0x641FA8C0(内存
  C0 A8 1F 64),旧版示例引文与历史 Trap 记录按冻结纪律不动
- UI 黄金基准:网口设置 4 态 + shot_info 共 5 张 .bin 由
  check_content_pages.py --update 重刷(渲染值 IP 变了,属预期变更)
- 保留判定:客户点表引用(IEC104/继电保护规划)、芯片手册摘录、
  日期冻结的变更记录行、已退役台架档案 MDK-ARM/retracted_bench_*

二、单一真源收口(A+B+C+D,用户批准全做)
- 新建 test/host/bench_defaults.py:台架 py 默认值唯一定义点
  (DEVICE_IP/DEVICE_GW/PC_IP/NET_BCAST),22 个脚本改为引用;
  MDK-ARM 两探针与 scripts/board_ui_nav.py 经 sys.path shim 引入
- test/board/net_test_task.h 新增 TEST_PC_PEER_IP,板测三处
  PHASE*_REMOTE_IP 改引该宏(保留 -D 覆盖);Phase1 断言改
  memcmp 数组比较,数组各只出现一次
- simulator/tests/check_content_pages.py 从 net_cfg.c 正则解析
  NET_CFG_DEF_* 推导期望值(解析失败即报错、不静默兜底),
  脚本内台架 IP 字面量清零
- 装饰性注释去具体值(net_cfg.h/net_socket.h/net_socket.c/
  net_socket_sim.c);字节序教学 hex 对刻意保留为唯一写值点
- 刻意不动:主机单测夹具 IP(逐文件钉住的任意值是正确测试
  实践)、文档正文 IP(那是文档内容本身)

顺带:scripts/gen_task_table.py 规范串清掉「,B5」——编号清退提交
72b4b094 改了受管区没重跑生成器,task-table 门禁因此一直红

验证:产品 @build 0/0;CH395F 套件开关构建 0/0 后还原产品态;
6 套主机单测 rc=0;content_pages(含宏推导路径)/菜单 8 态/
推流位比对全 PASS;bom-drift 99 无漂移;doc-links/coding-standard/
cppcheck 绿;py_compile 23 文件 + --help 冒烟(含 shim 绝对路径)
2026-11-02 14:46:42 +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 = 0x011FA8C0u; /* 192.168.31.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,31,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;
}