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 回填留待主工作树合并后。
737 lines
26 KiB
C
737 lines
26 KiB
C
/*
|
||
* 模块名称: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;
|
||
}
|