Files
STM32F4-Base/test/host/ftp_list_fill_test.py
T
wandering1 8b0ea17b71 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 / 无行尾空格。未推送。
2026-10-22 22:41:31 +08:00

121 lines
4.7 KiB
Python
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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LIST 完整性回归 —— 2026-10-22 起常驻
为什么需要:lftpd 的 send_list/send_nlst 原先把目录装进一块 static 1536 B 缓冲,装满就
break 并回 -1,cmd_list 据此回 **550**——目录一大,客户端拿到的是**静默截断**的清单外加
一个错误码,而清单看起来完全像"目录里就这些"。台架实测:30 条目的根目录只回了 24 条目
(1512 B),6 个文件凭空消失。设备目录清空后这个雷**再也暴露不出来**,所以需要本脚本
主动把目录撑过一块:造 N 个 8.3 短名小文件 → LIST 必须**全部列出**且完成回包是 226 →
逐个删掉还原。
判据(三条全过才 PASS):
1. N 个文件全部 STOR 成功(226)
2. LIST 全文里每个名字都在(少一个 = 截断回归)
3. LIST 的完成回包是 226 而不是 550(ftplib 在非 2xx 完成时会抛异常,正是这条)
清理:无论成败都逐个 DELE;删除失败会打 WARN(不影响判定,但会留残留文件要手清)。
用法:python ftp_list_fill_test.py [--host 192.168.1.100] [--files 26]
[--user admin] [--password admin]
注:--files 26 是按每行约 63 B 反推的下限(26×63 ≈ 1638 > 1536);目录本来就有
条目时更小也能触发,取 26 保证空目录也能越过一块。
"""
import argparse
import io
import sys
from ftplib import FTP, all_errors
HOST = "192.168.1.100"
PORT = 21
# 设备无匿名通道:账号取 /config.ini 的 [ftp],出厂默认 admin/admin
USER = "admin"
PASSWORD = "admin"
PREFIX = "ZZFT"
PAYLOAD = b"filler\r\n"
def names_of(count):
# FatFs 只支持严格 8.3(FF_USE_LFN=0):8 字符主名 + 3 字符扩展名
return ["%s%02d.TXT" % (PREFIX, i) for i in range(count)]
def main():
ap = argparse.ArgumentParser(description="FTP LIST completeness regression")
ap.add_argument("--host", default=HOST)
ap.add_argument("--port", type=int, default=PORT)
ap.add_argument("--files", type=int, default=26,
help="how many probe files to create (default 26, see docstring)")
ap.add_argument("--user", default=USER)
ap.add_argument("--password", default=PASSWORD)
args = ap.parse_args()
names = names_of(args.files)
stored = []
checks = []
ftp = FTP()
try:
ftp.connect(args.host, args.port, timeout=15)
# connect() 已经把 220 欢迎读走了(ftplib 语义),这里不能再 getresp()
print("login:", ftp.login(args.user, args.password))
ftp.voidcmd("TYPE I")
print("\n[1] STOR %d 个探针文件" % args.files)
for name in names:
try:
ftp.storbinary("STOR " + name, io.BytesIO(PAYLOAD))
stored.append(name)
except all_errors as exc:
checks.append(("STOR " + name, False, str(exc)[:60]))
break
if len(stored) == args.files:
checks.append(("STOR all %d files" % args.files, True, "ok"))
print("[2] LIST 全文校验")
lines = []
trunc_err = None
try:
ftp.retrlines("LIST", lines.append)
except all_errors as exc:
# 完成回包不是 2xx(老 bug 的 550 就走这里)
trunc_err = str(exc)
listed = " ".join(lines).upper()
print(" 收到 %d 行 / %d 字节" % (len(lines), sum(len(x) + 2 for x in lines)))
checks.append(("LIST 完成回包 226(非 550)", trunc_err is None,
trunc_err or "ok"))
missing = [n for n in stored if n.upper() not in listed]
checks.append(("LIST 列出全部 %d 个新文件" % len(stored), not missing,
"缺 " + ",".join(missing) if missing else "ok"))
except all_errors as exc:
checks.append(("会话", False, str(exc)[:80]))
finally:
print("[3] 清理")
left = 0
for name in stored:
try:
ftp.delete(name)
except all_errors as exc:
left += 1
print(" WARN: DELE %s 失败: %s" % (name, str(exc)[:50]))
if left:
print(" WARN: %d 个残留文件需手工清" % left)
try:
ftp.quit()
except all_errors:
pass
print("\n" + "=" * 44)
ok_all = True
for label, ok, detail in checks:
ok_all = ok_all and ok
print(" [%s] %s%s" % ("PASS" if ok else "FAIL", label,
"" if ok or not detail else " - " + detail))
print("=" * 44)
print(">>> TEST PASSED <<<" if ok_all else ">>> TEST FAILED <<<")
return 0 if ok_all else 1
if __name__ == "__main__":
sys.exit(main())