From a0c261b5eec6ec17646aa1492752784a38a17f0d Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 23 Oct 2026 23:16:56 +0800 Subject: [PATCH] =?UTF-8?q?test(NET/lftpd):=20lftpd=20=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E9=80=9A=E9=81=93=E6=A8=A1=E5=9D=97=EF=BC=8835=20=E6=9D=A1?= =?UTF-8?q?=EF=BC=89=EF=BC=8CFTP=20=E4=B8=80=E5=A5=97=E5=9B=9B=E5=9D=97?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E8=BF=9B=E5=88=86=E6=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 test/host/lftpd/,lftpd.c 覆盖 304/473 = 64.3%。做法是把生产源整个 #include 进 TU,于是命令表 commands[]、17 个 static cmd_* 与 handle_control_channel 都能直接驱动: 假 inet 层给"脚本化命令行队列 + 响应记录",假 io 层给"极简内存视图 + 可注入失败", 认证与配置当可注入黑盒(认证本身有 test/host/ftp_auth 的 10 条专测,不重复验)。 钉住的契约(都是真机上贵、却难按需复现的那几类): - STOR 传到一半写失败必须 **unlink 半成品**:否则 Flash 上永久留个坏文件,现场表现 是"升级包传坏了再也传不上去"。真机要卡准"传一半掐线"的时机,这里注一个短写就行。 - 数据通道是单连接 socket:LIST/RETR/STOR 用完必须关监听并把 data_socket 复位成 -1; accept 失败要报 425 不留残口,会话退出(QUIT 或对端断线)时 cleanup 也要关掉还没 用的被动监听。漏一次就是下一条命令拿着上一个号继续 accept,偶发连不上。 - 命令名缓冲只有 command_tmp[4+1],前置判断 if (index >= 5) 就是防 memcpy 越界; 恰好 5 个字符必须被拒(改一行这条就响)。 - 认证前置白名单:未登录只放行 USER/PASS/QUIT/FEAT/SYST/NOOP,其余一律 530;口令错 必须清 auth_user_ok 强制重走 USER;被锁与口令错都只回 530,不给攻击者区分提示; 配置读不到时 USER 无法匹配、PASS 直接 530(fail-closed)。 - 写开关([ftp] write_en=0)必须在**碰文件之前**挡住 STOR/DELE,且不去 accept 数据 通道;DELE 不许删目录。 - 227/229 编码:IP 经 net_htonl 后按高字节到低字节排(就是点分序),端口高/低两字节; 拿不到本机 IP 时要把刚建的监听关掉。FEAT 多行:首行 211-、四条能力无状态码、末行 211 结束(共 6 条响应)。 - 220 欢迎没送达(对端已走)就直接收工,不空转读命令;该留痕按真实分级算 INFO 而不是 ERROR(真实 private/lftpd_log.h 把 lftpd_log_peer 归到 INFO:任何人连 21 端口就走 就能无限制造固件级 ERROR,还会撞掉 @test 的 Forbidden 标记 [ERR])。 - 传输循环每轮喂狗(lftpd_task_wd_beat 计数可断言)。 主机侧两个必要的兼容处理(都写在桩注释里,不是随手改): 1. lftpd.c 会 include ff.h,而这份 FatFs 在 Windows 支路上把 winsock.h 拉进来,它带 "#define s_addr S_un.S_addr" 的 BSD 兼容宏,会把真实 net_types.h 里的字段声明 uint32_t s_addr; 展开成 uint32_t S_un.S_addr; 直接炸(实测 C2143 在 "." 前缺 ";")。 桩里只 #undef 这个污染宏,让固件那份结构体定义原样参与编译,被测代码读的还是真字段。 2. 引号 include 先从 lftpd.c 自己目录找 private/lftpd_log.h,桩目录赢不过它——于是 顺着用真实那份(它只是把 lftpd_log_* 映射成 DBG_*),只桩最底下的 dbg_log.h。这样 真实日志分级也一起被编译验证,比我自己重写一套更对。 自查(35 条里前两轮红了 4 条,全是**我的期望/前置态写错**,实现均正确):未登录时 FOO 被白名单以 530 拦掉测不到 502 分派;227 的 IP 顺序我把 htonl 前后搞反;FEAT 是 6 条响应我按 5 条索引;PASV 之后走完整会话会被 cleanup 关掉监听(那正是 006 要验的 行为),观察 PASV 留下的状态必须直调 cmd_pasv。 覆盖率与台账:套件 26 → 27 全绿;files_tested 30 → 31;未触及 .c 36 → 35;分母 4,072 → 4,545 行。**总体 line_rate 88.4% → 85.9% 是摊薄不是回退**(64.3% 的文件进 了分母),按 §5 口径要盯 files_tested/files_untested——它从 19 一路涨到 31。 恒等式 35 = 1(P1 剩 iec104_serv.c) + 5(P2) + 29(P3)。编号补建 TC-HOST-LFTPD (游标 034),族数 20 → 21。 自查门禁:test-cppcheck Passed;check_ids --strict 通过;doc-links 35 处(= 存量); 9 个文件 BOM/CR/U+FFFD/行尾逐项净。零生产代码改动,未跑 @build。--all-files 仍只有 mem-numbers / mem-budget(.map 台账漂移,等 Lead 合并后统一回填)与 ui-golden (ref/*.bin 不入库)三条失败,与本分支改动无关。 --- docs/测试说明/主机单测覆盖率口径.md | 7 +- scripts/id_registry.json | 22 + scripts/test_coverage_baseline.json | 41 +- test/host/lftpd/lftpd_test.c | 736 ++++++++++++++++++++++++++++ test/host/lftpd/run_unit_test.py | 62 +++ test/host/lftpd/stub/FreeRTOS.h | 24 + test/host/lftpd/stub/dbg_log.h | 29 ++ test/host/lftpd/stub/net_socket.h | 34 ++ test/host/lftpd/stub/task.h | 18 + 9 files changed, 956 insertions(+), 17 deletions(-) create mode 100644 test/host/lftpd/lftpd_test.c create mode 100644 test/host/lftpd/run_unit_test.py create mode 100644 test/host/lftpd/stub/FreeRTOS.h create mode 100644 test/host/lftpd/stub/dbg_log.h create mode 100644 test/host/lftpd/stub/net_socket.h create mode 100644 test/host/lftpd/stub/task.h diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 003a2bc3..2d25a9ab 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -222,9 +222,10 @@ |---|---|---|---| | 配置与工具 4 块 | ✅ 完成 | `test/host/{net_cfg,ftp_cfg,prot_set_store,task_prof}`(104 条) | `net_cfg.c` 108/112、`task_prof.c` 88/93、`ftp_cfg.c` 35/36、`prot_set_store.c` 27/29 | -| FTP 一套(4 块里 3 块) | ✅ 大部分 | `test/host/lftpd_io`(33 条)、`test/host/lftpd_inet`(28 条) | - `lftpd_inet.c` **127/127 = 100%**(全仓第一个满覆盖文件)、`lftpd_string.c` 8/10、 - `lftpd_io.c` 85/159(`read/write/close/stat/unlink/readdir` 待假 `FIL/FILINFO` 字段语义) | +| FTP 一套(4 块全进分母) | ✅ 主体完成 | `test/host/lftpd_io`(33 条)、`test/host/lftpd_inet`(28 条)、 + `test/host/lftpd`(35 条) | `lftpd_inet.c` **127/127 = 100%**、`lftpd_io.c` 85/159、 + `lftpd.c` **304/473 = 64.3%**(控制通道闸门 + 17 条命令 + 被动模式 + 传输收尾都进来了; + 还剩 `lftpd_start` 主循环与 `send_list`/`send_file` 的分批边界分支) | | NET 四块之一:`net_select.c` | ✅ 完成 | `test/host/net_select`(21 条) | `net_select.c` 97/114 = **85.1%** | | `lftpd.c` / `lftpd_inet.c` | ⬜ 未开始 | — | 需假 `net_socket` 层(`time_sync`/stub 那份可扩展) | | NET 三块之一:`net_listener.c` | ✅ 完成(**125/126 = 99.2%**) | diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 8c18c423..55fd0dca 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1834,6 +1834,28 @@ "subitem_ok": true, "owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 P1 FTP 批次补登记", "cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌" + }, + { + "name": "TC-HOST-LFTPD", + "class": "TC", + "title": "主机侧单测用例号:FTP 控制通道协议层(test/host/lftpd)", + "pattern": "\\bTC-LFTPD-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/lftpd/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-LFTPD-034", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 P1 FTP 批次补登记", + "cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌" } ], "unmanaged_bare": { diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 165d8140..eb75b817 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -160,6 +160,14 @@ "lines_covered": 123, "lines_valid": 253 }, + "Drivers/BSP/NET/lftpd/lftpd.c": { + "func_rate": 0.8077, + "funcs_covered": 21, + "funcs_valid": 26, + "line_rate": 0.6427, + "lines_covered": 304, + "lines_valid": 473 + }, "Drivers/BSP/NET/lftpd/lftpd_auth.c": { "func_rate": 1.0, "funcs_covered": 2, @@ -302,6 +310,14 @@ "lines_total": 1193, "ok": true }, + "lftpd": { + "files": { + "Drivers/BSP/NET/lftpd/lftpd.c": 473, + "Drivers/BSP/NET/lftpd/lftpd_string.c": 7 + }, + "lines_total": 480, + "ok": true + }, "lftpd_inet": { "files": { "Drivers/BSP/NET/lftpd/lftpd_inet.c": 127 @@ -467,6 +483,7 @@ "App/util/task_prof.c", "App/util/task_watchdog.c", "Drivers/BSP/GD5F2GQ5UE/nand_ftl.c", + "Drivers/BSP/NET/lftpd/lftpd.c", "Drivers/BSP/NET/lftpd/lftpd_auth.c", "Drivers/BSP/NET/lftpd/lftpd_inet.c", "Drivers/BSP/NET/lftpd/lftpd_io.c", @@ -480,16 +497,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 30, - "files_untested": 114, - "func_rate": 0.9097, - "funcs_covered": 272, - "funcs_valid": 299, - "line_rate": 0.8838, - "lines_covered": 3599, - "lines_valid": 4072, - "modules_ok": 26, - "modules_total": 26 + "files_tested": 31, + "files_untested": 113, + "func_rate": 0.9015, + "funcs_covered": 293, + "funcs_valid": 325, + "line_rate": 0.8587, + "lines_covered": 3903, + "lines_valid": 4545, + "modules_ok": 27, + "modules_total": 27 }, "untested_sources": [ { @@ -500,10 +517,6 @@ "code_lines": 801, "path": "Drivers/BSP/CH395F/ch395f.c" }, - { - "code_lines": 694, - "path": "Drivers/BSP/NET/lftpd/lftpd.c" - }, { "code_lines": 689, "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c" diff --git a/test/host/lftpd/lftpd_test.c b/test/host/lftpd/lftpd_test.c new file mode 100644 index 00000000..c22922cd --- /dev/null +++ b/test/host/lftpd/lftpd_test.c @@ -0,0 +1,736 @@ +/* + * 模块名称: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 +#include +#include + +#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 循环跑飞(>200 轮)\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 未登录只放行 USER/PASS/QUIT/FEAT/SYST/NOOP,其它一律 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 命令名 >=5 字符先拒(command_tmp 只有 5 格,防 memcpy 越界)"); + + 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 认不出的命令回 502 而不是断开"); + + 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 QUIT 之后不再读下一条(会话就此结束)"); + + 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 欢迎未送达(对端已走)就直接收工,不空转读命令(该留痕按真实分级算 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 退出时必须释放还开着的**数据监听**(单连接 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 命令名大小写不敏感"); + + 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 没先开被动模式的数据命令一律 425,且不去 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 227 六元组:IP 经 htonl 后按高字节到低字节(点分序)、端口高/低,并记下被动监听号"); + + 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 拿不到本机 IP 时要把刚建的监听关掉(别留半个被动模式)"); + + 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 只报端口(IPv6 客户端自己沿用控制连接地址)"); + + 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 数据 accept 失败:报 425 并**关掉监听复位**,不留给下一条命令"); + + 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 成功:先 150、数据口写目录、再 226 收尾"); + + 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 目录打不开时 LIST 回 550(不是 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 成功:226 收尾、留 DOWNLOAD 记录,且传输循环有喂狗"); + + 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 文件打不开回 450(传输失败与命令不合法要分开报)"); + + 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 STOR 中途失败必须**删掉半成品**(否则 Flash 上永久留个坏文件)"); + + 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 写开关关闭时 STOR 在碰文件之前就拒绝,且不开数据通道"); + + 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 写开关同样挡住 DELE"); + + 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 不许删目录(本实现只支持删文件)"); + + 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 删普通文件走 unlink 并回 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 空参数拒绝,当前目录不变"); + + reset_all(); + g_io.is_dir = 0; + (void)cmd_cwd(&g_cli, "a.txt"); + CHECK(rep_has("550 ") == 1, "TC-LFTPD-023b CWD 到普通文件被拒(is_dir 必须真为目录)"); + + 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 CWD 成功后更新当前目录并释放路径缓冲(不泄漏堆)"); + + reset_all(); + (void)cmd_cwd(&g_cli, "/"); + CHECK((rep_has("250 ") == 1) && (g_io.stat_rc == 0), + "TC-LFTPD-024b 回到根目录免 stat 直接放行(根一定存在)"); + + 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 报字节数"); + + reset_all(); + g_io.stat_rc = -1; + (void)cmd_size(&g_cli, "nope"); + CHECK(rep_has("550 ") == 1, "TC-LFTPD-025b 文件不存在时 SIZE 回 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 用双引号包住目录(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 固定报 UNIX Type: L8(客户端据此决定模式)"); + + 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 多行:首行 211-、四条能力无状态码、末行 211 结束"); + + reset_all(); + (void)cmd_type(&g_cli, "I"); + (void)cmd_noop(&g_cli, NULL); + CHECK(rep_has("200 ") == 2, "TC-LFTPD-030 TYPE 与 NOOP 都回 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 记下用户名与匹配位,PASS 过了就转已登录"); + + 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 口令错:530 且**必须重新走 USER**(auth_user_ok 已清零)"); + + 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 连错被锁时同样只回 530(不给攻击者区分提示)"); + + 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 配置缺失时 USER 无法匹配、PASS 直接 530(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; +} diff --git a/test/host/lftpd/run_unit_test.py b/test/host/lftpd/run_unit_test.py new file mode 100644 index 00000000..5672f3cd --- /dev/null +++ b/test/host/lftpd/run_unit_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""FTP 控制通道协议层(lftpd)主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/lftpd/lftpd_test.c(以 #include 纳入生产源 + Drivers/BSP/NET/lftpd/lftpd.c),在 PC 上离线驱动控制通道状态机与 17 条命令 + 处理器:认证前置白名单、超长命令名拒绝、未知命令 502、QUIT 收尾释放数据监听、 + 大小写不敏感、PASV/EPSV 的 227/229 编码、数据 accept 失败不残留半开监听、 + STOR 中途失败删半成品、写开关挡住 STOR/DELE、CWD 目录判定与堆释放、FEAT 多行 + 格式、登录序列与 fail-closed。工具链与判定委托 unit_test_harness.py。 + +为什么要主机侧单测(而不是只用台架): + STOR 传一半失败必须把半成品删掉,否则 Flash 上永久留个坏文件(现场表现为升级包 + 传坏了再也传不上去),真机要卡准"传一半掐线"的时机极难;数据通道是单连接 socket, + 每条命令用完必须关监听并复位 data_socket=-1,漏一次就是偶发连不上且与上一会话 + 时序相关;command_tmp 只有 5 格,那条 index >= 5 的前置判断就是防 memcpy 越界, + 改一行就没了;写开关必须在碰文件之前挡住。这些都要能注入失败才测得干净。 + +桩的边界:lftpd.h / private/*.h / lftpd_auth.h / ftp_cfg.h / ff.h / net_types.h 用真实 + 头(命令表、client 结构、状态码宏、ftp_cfg_t 字段一改即编译期报错);下面三层 + (inet 适配、io 层、认证与配置)是假件——认证本身有 test/host/ftp_auth 的 10 条 + 专测,本模块只把它当可注入的黑盒,不重复验;认证状态结构体按黑盒 memset 清零, + 不写死字段名。假 inet 给"脚本化命令行队列 + 响应记录",假 io 给"极简内存视图 + + 可注入失败",另配 vTaskDelay 跑飞保险。 + +用法(仓库根目录): + python test/host/lftpd/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/测试说明/FTP_Test_Guide.md、docs/子系统设计/网络配置说明.md §3.4、 +docs/测试说明/主机单测覆盖率口径.md §9 +创建日期:2026-10-23 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +BUILD = TestBuild( + name="LFTPD Protocol Tests", + src=os.path.join(HERE, "lftpd_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "Drivers", "BSP", "NET", "lftpd"), + os.path.join(ROOT, "Drivers", "BSP", "NET"), + os.path.join(ROOT, "Drivers", "BSP", "NETCFG"), # ftp_cfg.h 落在这里 + os.path.join(ROOT, "Lib", "FatFs"), + HERE, + ], + extra_flags=["/D_CRT_SECURE_NO_WARNINGS"], + summary_marker="=== LFTPD Protocol Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) + diff --git a/test/host/lftpd/stub/FreeRTOS.h b/test/host/lftpd/stub/FreeRTOS.h new file mode 100644 index 00000000..2465f807 --- /dev/null +++ b/test/host/lftpd/stub/FreeRTOS.h @@ -0,0 +1,24 @@ +/* + * 模块名称:主机侧单测桩 - FreeRTOS.h(lftpd 协议层模块专用) + * 模块功能:宿主等价的 tick 类型与换算(本仓 tick=1ms,故 pdMS_TO_TICKS 即原值), + * 让 FTP 的 3 s 监听重建退避、LFTPD_WAIT_SLICE_MS 分片可直接按毫秒断言。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef FREERTOS_H +#define FREERTOS_H + +#include +#include + +typedef uint32_t TickType_t; +typedef long BaseType_t; +typedef unsigned long UBaseType_t; +typedef void *TaskHandle_t; + +#define pdMS_TO_TICKS(ms) ((TickType_t)(ms)) + +void *pvPortMalloc(size_t xWantedSize); +void vPortFree(void *pv); + +#endif /* FREERTOS_H */ diff --git a/test/host/lftpd/stub/dbg_log.h b/test/host/lftpd/stub/dbg_log.h new file mode 100644 index 00000000..4fe48470 --- /dev/null +++ b/test/host/lftpd/stub/dbg_log.h @@ -0,0 +1,29 @@ +/* + * 模块名称:主机侧单测桩 - dbg_log.h(lftpd 协议层模块专用) + * 模块功能:DBG_* 换成计数,让"对端在 220 握手前就走了""传输未完成"这些只体现在 + * 日志里的分支可断言。 + * + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 为什么桩这层而不是 private/lftpd_log.h:真实那份是**宏**(lftpd_log_info 等直接 + * 展开成 DBG_*),而且 MSVC 的引号 include 会先从 lftpd.c 自己的目录找 + * private/lftpd_log.h——桩目录赢不过它。所以正确做法是让真实 lftpd_log.h 照常参与 + * 编译(它的分级语义也就一起被验证了),只把最底下的 DBG_* 换成计数。 + * 顺带说明分级(真实头注释里写得很清楚):lftpd_log_peer 是 **info** 级不是 error, + * 因为"客户端关了个窗口"不该把 @test 的 Forbidden 标记 [ERR] 触发成 FAIL,也不该 + * 让任何人连 21 端口就走就能无限制造固件级 ERROR;而 DOWNLOAD/UPLOAD FAILED 仍记 + * error(那是数据完整性事件)。 + * + * 创建日期:2026-10-23 + */ +#ifndef DBG_LOG_H +#define DBG_LOG_H + +extern int g_lg_info_count; +extern int g_lg_error_count; +extern int g_lg_debug_count; + +#define DBG_INFO(fmt, ...) do { (void)(fmt); g_lg_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_lg_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_lg_error_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/lftpd/stub/net_socket.h b/test/host/lftpd/stub/net_socket.h new file mode 100644 index 00000000..438854e2 --- /dev/null +++ b/test/host/lftpd/stub/net_socket.h @@ -0,0 +1,34 @@ +/* + * 模块名称:主机侧单测桩 - net_socket.h(lftpd 协议层模块专用) + * 模块功能:给 FTP 协议层四个地址/字节序入口(PASV 要用本机 IP 拼 227 响应), + * 并照做真实头对 FreeRTOS.h / task.h 的 include 链。地址结构与 + * NET_AF_INET / NET_SOCK_STREAM 取**真实** net_types.h。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef NET_SOCKET_H +#define NET_SOCKET_H + +#include "FreeRTOS.h" +#include "task.h" + +/* 主机侧必要清理:lftpd.c 会 include ff.h,而这份 FatFs 在 Windows 支路上会把 + * winsock.h 拉进来,它带着 "#define s_addr S_un.S_addr" 这个 BSD 兼容宏——于是真实 + * net_types.h 里的字段声明 uint32_t s_addr; 被展开成 uint32_t S_un.S_addr; 直接炸 + * (实测报 C2143:在 "." 前面缺 ";",再级联出一串"sin_addr 使用未定义的 struct")。 + * 这里只 #undef 掉这个污染宏,让**固件那份结构体定义原样参与编译**:字段名仍是真实 + * 的 s_addr,被测代码读的也是它。(lftpd_inet 批次不 include ff.h,所以没撞上。) */ +#ifdef s_addr +#undef s_addr +#endif + +#include "net_types.h" + +int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen); +int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen); +uint16_t net_htons(uint16_t hostshort); +uint16_t net_ntohs(uint16_t netshort); +uint32_t net_htonl(uint32_t hostlong); +char *net_inet_ntoa(uint32_t in, char *p_buf); /* 两参:调用方给缓冲 */ + +#endif /* NET_SOCKET_H */ diff --git a/test/host/lftpd/stub/task.h b/test/host/lftpd/stub/task.h new file mode 100644 index 00000000..4369b6d4 --- /dev/null +++ b/test/host/lftpd/stub/task.h @@ -0,0 +1,18 @@ +/* + * 模块名称:主机侧单测桩 - task.h(lftpd 协议层模块专用) + * 模块功能:vTaskDelay 与 xTaskGetTickCount 的可控假件接口。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 注意 lftpd.c 只 include 了 FreeRTOS.h,这两个符号在真机上是经 net_socket.h 的 + * include 链拿到的——所以本模块的 net_socket.h 桩必须照做那条链(少一行就是 C2065, + * lftpd_inet 批次实测踩过)。 + * 创建日期:2026-10-23 + */ +#ifndef TASK_H +#define TASK_H + +#include "FreeRTOS.h" + +TickType_t xTaskGetTickCount(void); +void vTaskDelay(TickType_t xTicksToDelay); + +#endif /* TASK_H */