#!/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 <设备IP>] [--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 import bench_defaults # 台架连接默认值单一真源 HOST = bench_defaults.DEVICE_IP 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())