b75439c以"命令行是多余的"为由把读取改成 FileStream+StreamReader,是错误前提: 项目存在无法用标准文件 I/O 读取的加密 txt/钻带文件(CLAUDE.md "Encrypted file reading"、 Docs/readme.md "读取加密钻带(使用cmd命令type读取)"、 Docs/刀具列表拖动排序功能说明.md "加密文件:使用 cmd 命令读取"均有记录), 该改动会导致这些文件读取失败。本次回退。 改动: - 还原 CommandTypeFileReader.cs:cmd.exe /d /c type(ANSI/936 解码), 失败时退出码与 stderr 保留在异常消息里 - 删除 ForbiddenPathChars 与两处字符校验(本次报错根因): ( ) & ^ @ 空格在双引号内对 type 安全,黑名单只会误伤 (D2-f)、(1) 等正常文件名 - 保留"非空路径""文件存在"校验与 ReadAllText(path, encoding) 重载 - 删除 AnsiFileReader.cs,15 处调用点改回 - CLAUDE.md / .trellis/spec/:恢复"加密文件必须用 cmd type"的说明, 并标注为不可优化掉的硬约束(含b75439c反例) 验证:demo_drl 全部 36 个文件与2335894实现逐字符一致、行数一致; 含 ( ) & 空格路径实测可读(旧黑名单实现抛"文件路径包含不允许的命令行特殊字符"); cmd 失败消息含退出码与 stderr;dotnet build -p:Platform=x86 0 错误, 警告集与 HEAD 基线完全一致。
5.7 KiB
含特殊字符路径的钻带文件读取修复说明
修复日期:2026-09-20(任务
.trellis/tasks/09-20-restore-cmd-type-reader) 影响面:所有钻带/参数文件读取入口(共 15 处调用) 本文件替换了同日先前一版的错误结论(“不存在加密层 / 可不经命令行”),见下文“上一轮的错误结论”。
问题描述
选择路径或文件名中含特定字符的钻带/参数文件时,加载直接失败:
加载钻带文件失败:文件路径包含不允许的命令行特殊字符(Parameter 'filePath')
命中字符包括 ( ) & % ! ^ @ | < > 等。实际业务文件名很容易命中,例如:
1009B-2013钻机参数表(D2-f)(A1A2厂) (1).docdemo_drl (1)/xx.drl
根因
读取统一入口 CommandTypeFileReader.ReadAllText 通过 cmd.exe /d /c type "<path>" 读取文件:
FileName = "cmd.exe",
Arguments = $"/d /c type \"{filePath}\"",
因为路径会成为命令行的一部分,03d9b8b(2026-05-16)在 ValidateAndNormalizePath 中引入了一串
字符黑名单(ForbiddenPathChars = & | < > ^ % ! ( ) @ " \r \n),命中即抛异常。
这层字符黑名单是过度防御:真正会被 cmd 语义破坏的只有 %VAR% 形状片段(变量展开在双引号内同样发生)
和延迟展开开启时的 !,而 ( ) & ^ @ 空格在双引号内完全安全。黑名单把安全字符一并禁掉,
直接误伤 (D2-f)、(1) 这类正常业务文件名。
为什么必须保留 cmd.exe /d /c type(而不能改成标准文件 I/O)
项目存在部分加密的 txt/钻带文件,无法用标准文件 I/O(File.ReadAllText / FileStream / StreamReader)
直接读取,只能靠 type 命令读出明文。这条项目事实原本就记录在 CLAUDE.md:
Encrypted file reading: Uses
cmd.exe /c typeto read drill tape files (some files are "encrypted" and cannot be read directly via standard file I/O)
上一轮的错误结论(已被本次回退)
b75439c(2026-09-20)以“type 只是字节透传、命令行是多余的”为前提,把读取改成
FileStream + StreamReader 直读并删除 CommandTypeFileReader。该前提是错的:
- 该提交的动机来自历史问题(.NET 6 下
Encoding.Default= UTF-8 导致 GB2312 乱码, 当年Encoding.GetEncoding(936)因未注册 CodePages provider 会抛异常)。编码问题确实早已由473b2ca(注册CodePagesEncodingProvider)解决,但**“编码问题已解决”不等于“cmd type 可以删”**—— 保留 cmd type 的真实理由是加密文件。 - 上轮检索时关键词只用了
cmd type,而CLAUDE.md的记录写作cmd.exe /c type,因此漏检, 随后复核又把这条项目事实当作“没有证据的说法”删除。
因此本次恢复 CommandTypeFileReader,并在 CLAUDE.md 与 .trellis/spec/ 中把
“必须经 cmd type 读取加密文件、禁止改成标准文件 I/O”写成硬约束,避免再次被“优化掉”。
本次改动
- 恢复
CommandTypeFileReader.cs(git show 2335894:CommandTypeFileReader.cs为基线), 读取仍为cmd.exe /d /c type "<path>",StandardOutputEncoding/StandardErrorEncoding仍为 ANSI/936; 失败时保留退出码与 stderr:InvalidOperationException("读取文件失败,退出代码:{code},错误:{stderr}")。 - 删除
ForbiddenPathChars与ValidateAndNormalizePath中两处字符校验(原始路径一次、规范化路径一次)。 路径只保留“非空”ArgumentException("文件路径不能为空")与“文件存在”FileNotFoundException("文件不存在:...")。 - 删除
b75439c引入的AnsiFileReader.cs;15 处调用点由AnsiFileReader.ReadAllText改回CommandTypeFileReader.ReadAllText。 - 保留
ReadAllText(filePath, encoding)重载;默认编码仍为 ANSI/GB2312(code page 936)。 - 修正
CLAUDE.md、.trellis/spec/backend/、.trellis/spec/guides/index.md中上一轮被改错的描述。
实测结论(Windows 10.0.26100,cmd /d /c type "<path>")
| 路径中的字符 | 实测结果 | 说明 |
|---|---|---|
( ) & ^ @ 空格 |
可正常读取 | 双引号内 cmd 不把 &/^ 当分隔符;本次黑名单移除的直接依据 |
%CD% / %TEMP% / %USERNAME% |
读取失败(rc=1) | 变量展开在双引号内依然发生 —— 已知残留限制 |
!CD! |
默认可读;/v:on 时失败 |
依赖延迟展开开关 —— 已知残留限制 |
< > ` |
"` 与 CR/LF |
在 Windows 文件名中本就非法 |
含 0x1A 等二进制字节 |
字节完全一致 | type 是字节透传,不做任何转换 |
已知残留限制(cmd 语义,本次不解决)
- 路径中出现
%VAR%形状片段时会变量展开,导致读取失败。 - 路径中的
!仅在延迟展开开启(cmd /v:on或注册表DelayedExpansion=1)时失败。
这两类片段无法通过字符黑名单与正常文件名区分,故不拦截;如需彻底解决需另立任务 (例如改用短路径/临时副本等 cmd 之外的方案),本次范围不含。
后续约定
- 钻带/参数文件读取统一走
CommandTypeFileReader.ReadAllText:必须保留cmd.exe /d /c type, 不要为了“去掉命令行”改成File.ReadAllText/File.ReadAllBytes/FileStream/StreamReader(加密文件会读取失败,b75439c已试过并被回退)。 - 不要再对文件路径做字符黑名单校验:双引号内
( ) & ^ @与空格对type安全,黑名单只会误伤正常文件名。 - 需要更换编码时使用
ReadAllText(filePath, encoding)重载,不要另起第二套读取逻辑。