fix(lftpd): LIST/NLST 满块续发——大目录不再被静默截断并回 550
现象:`ftp_test.py` 的 `LIST 150->226` 断言失败——1512 B 数据全收到了,完成回包却是 **550**。 **根因**(`send_list`/`send_nlst` 各一处):定案三把"整份目录一次性批量发送"落进 static 1536 B 缓冲时,顺手留下了"装满就 `break` 并回 −1",`cmd_list` 据此回 550。于是目录一旦超过一块,客户端 拿到的是**静默截断**的清单**外加一个错误码**——而清单看起来完全像"目录里就这些"。这比单纯报错 更危险:没有人会去数条目个数。 **台架实测(同一台设备、同一个根目录,改动前后各测一次)**: · 改前:1512 B / **24 条目** / 完成回包 **550**(停在 A0007.BIN,后面 6 个文件被吞) · 改后:1880 B / **30 条目** / 完成回包 **226** **修法:满块续发**,而不是把缓冲区加大。新增 `list_batch_flush()`——装满一块就整块发出、清空 继续装,收尾再发最后一块。为什么仍要"批量":定案三的本体是实测结论——同一连接上背靠背 ~60 B 小段写会毒化 CH395F 的 TX(芯片报 SEND_OK 但对端全丢,无 RST 无 DISCONNECT),而 RETR 的 1 KB 满块连发安全;所以这里只发大块,绝不退回"每行一发"。单行真装不下时**明确失败**而不是少发条目 (8.3 短名 + 定长格式远小于 1536,真发生说明格式被改坏)。两条出口的 `lftpd_io_closedir` 收敛到 循环后单点:卷锁持有至 closedir,任何提前 return 都会漏锁。 **防回退**(关键):修完之后,**目录清空的设备上这颗雷再也暴露不出来**,所以补了 `test/host/ftp_list_fill_test.py`——自造 26 个 8.3 短名文件把清单撑过一块,三条判据(STOR 全 226 /LIST 全文里每个名字都在/完成回包 226 而非 550,用 ftplib `retrlines` 让非 2xx 完成直接抛), 跑完逐个 DELE 还原。真机实测 `收到 56 行 / 3544 字节`(跨 3 块,满块续发被真正走到)→ 三条全 PASS。用法登记进 `docs/测试说明/FTP_Test_Guide.md` §4.5,机理与增补写进 `docs/问题记录/CH395F_Trap_Records.md` 定案三。 **顺带记录的已知偏差(未修)**:`cmd_list`/`cmd_nlst` 忽略命令参数、恒列会话 CWD (`client->directory`)。主流客户端都是 `CWD` + 裸 `LIST`,实际影响小,先记在案。 体量:`Code` 213,264 → **213,436 B(+172)**、RO 不变 → `ER_IROM1` **232,088 B(22.1%) **、 `ROM 合计` 233,276 B;**RAM 一字未动**(`RW_IRAM1` 75,264 B 57.4% / `RW_CCM` 57,376 B 87.9%)。 数字联动照做:`data/ram_report` 重跑、`ram_baseline --update/--write`、 `check_mem_numbers --write`、 FLASH 配图常量重算重生成(桶合计 = 232,088 与 `Code+RO` 相符)、报告 §1.3/§5.1/§5.4 与部署 §3.3。 门禁:`@build` 0 错 0 警告;`@test` PASS(692 ms);`ftp_test.py` → **TEST PASSED**(含此前失败 的 `LIST 150->226`);`ftp_up_dl_test.py` → TEST PASSED;`ftp_list_fill_test.py` → TEST PASSED; `doc-links --all` 146 文档 0 问题;`id-check --strict` 增量拦截通过(可判定 82.82%); `mem-numbers` / `mem-budget` 通过;改动文件字节卫生 BOM 无 / CR 0 / U+FFFD 0 / 无行尾空格。未推送。
This commit is contained in:
@@ -104,6 +104,32 @@ static int send_response(int socket, int code, bool include_code,
|
||||
#define send_multiline_response_end(socket, code, format, ...) \
|
||||
send_response(socket, code, true, false, format, ##__VA_ARGS__)
|
||||
|
||||
/* 一块装满就整块发出、清空继续装。
|
||||
* 为什么仍要"批量":定案三(../问题记录/CH395F_Trap_Records.md,docs 里那条 LIST 吞包
|
||||
* 定案)实测同一连接上**背靠背 ~60 B 小段写**会毒化 CH395F 的 TX(芯片报 SEND_OK 但对端
|
||||
* 全丢,无 RST 无 DISCONNECT),而 RETR 的 1 KB 满块连发安全——所以这里只发大块,
|
||||
* 绝不退回"每行一发"。
|
||||
* 为什么必须"续发"而不是截断:原先装满就 break 并回 -1,cmd_list 据此回 550,客户端
|
||||
* 既拿到**不完整**的目录、又收到错误码(实测 24 条目/1512 B 之后全部被吞)。目录大
|
||||
* 小不该由一个缓冲区上限决定。
|
||||
* 返回:0 = 已发出(或本就空),-1 = 写失败 */
|
||||
static int list_batch_flush(int socket, char *batch, int *p_blen)
|
||||
{
|
||||
int len = *p_blen;
|
||||
|
||||
if (len <= 0) {
|
||||
return 0;
|
||||
}
|
||||
lftpd_task_wd_beat(); /* 看门狗:目录列表分块发送处 */
|
||||
if (lftpd_inet_write(socket, batch, len) < 0) {
|
||||
return -1;
|
||||
}
|
||||
*p_blen = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static int send_list(int socket, const char *path)
|
||||
{
|
||||
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
|
||||
@@ -118,11 +144,9 @@ static int send_list(int socket, const char *path)
|
||||
uint32_t size;
|
||||
int is_dir;
|
||||
|
||||
/* N30-PROBE(临时实验):整份目录一次性发送(验证"背靠背多小段
|
||||
* 毒化芯片 TX"假设——每行一个 net_send 时 LIST#4 必挂) */
|
||||
static char batch[1536]; /* static: off-stack */
|
||||
int blen = 0;
|
||||
int trunc = 0;
|
||||
int fail = 0;
|
||||
|
||||
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
|
||||
int n;
|
||||
@@ -133,25 +157,45 @@ static int send_list(int socket, const char *path)
|
||||
n = snprintf(batch + blen, sizeof(batch) - blen, file_format,
|
||||
(unsigned long)size, name);
|
||||
}
|
||||
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
|
||||
trunc = 1;
|
||||
if (n < 0) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
if ((size_t)n >= sizeof(batch) - blen) {
|
||||
if (list_batch_flush(socket, batch, &blen) != 0) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
/* 单行就超过整块 → 明确失败,绝不静默少发条目
|
||||
* (8.3 短名 + 定长格式远小于 1536,真发生说明格式被改坏) */
|
||||
if ((size_t)n >= sizeof(batch)) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
if (is_dir) {
|
||||
n = snprintf(batch, sizeof(batch), directory_format,
|
||||
(unsigned long)size, name);
|
||||
} else {
|
||||
n = snprintf(batch, sizeof(batch), file_format,
|
||||
(unsigned long)size, name);
|
||||
}
|
||||
if (n < 0 || (size_t)n >= sizeof(batch)) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
blen += n;
|
||||
}
|
||||
|
||||
lftpd_io_closedir(dp);
|
||||
lftpd_io_closedir(dp); /* 所有出口都必须走到:卷锁持有至 closedir */
|
||||
|
||||
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
|
||||
if (blen > 0) {
|
||||
if (!fail && blen > 0) {
|
||||
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
|
||||
if (lftpd_inet_write(socket, batch, blen) < 0) {
|
||||
return -1;
|
||||
fail = 1;
|
||||
}
|
||||
}
|
||||
if (trunc) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
return fail ? -1 : 0;
|
||||
}
|
||||
|
||||
static int send_nlst(int socket, const char *path)
|
||||
@@ -165,34 +209,44 @@ static int send_nlst(int socket, const char *path)
|
||||
uint32_t size;
|
||||
int is_dir;
|
||||
|
||||
/* N30-PROBE(临时实验):同 send_list,一次性批量发送 */
|
||||
static char batch[1536]; /* static: off-stack */
|
||||
int blen = 0;
|
||||
int trunc = 0;
|
||||
int fail = 0;
|
||||
|
||||
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
|
||||
if (!is_dir) {
|
||||
int n = snprintf(batch + blen, sizeof(batch) - blen, "%s\r\n", name);
|
||||
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
|
||||
trunc = 1;
|
||||
if (n < 0) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
if ((size_t)n >= sizeof(batch) - blen) {
|
||||
if (list_batch_flush(socket, batch, &blen) != 0) { /* 同 send_list:满块续发 */
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
if ((size_t)n >= sizeof(batch)) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
n = snprintf(batch, sizeof(batch), "%s\r\n", name);
|
||||
if (n < 0 || (size_t)n >= sizeof(batch)) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
blen += n;
|
||||
}
|
||||
}
|
||||
|
||||
lftpd_io_closedir(dp);
|
||||
lftpd_io_closedir(dp); /* 所有出口都必须走到:卷锁持有至 closedir */
|
||||
|
||||
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
|
||||
if (blen > 0) {
|
||||
if (lftpd_inet_write(socket, batch, blen) < 0) {
|
||||
return -1;
|
||||
if (!fail && blen > 0) {
|
||||
if (lftpd_inet_write(socket, batch, blen) < 0) { /* 成功返回字节数,非 0=成功 */
|
||||
fail = 1;
|
||||
}
|
||||
}
|
||||
if (trunc) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
return fail ? -1 : 0;
|
||||
}
|
||||
|
||||
static int send_file(int socket, const char *path)
|
||||
|
||||
Reference in New Issue
Block a user