Files
AohDrllTools/Docs/含特殊字符路径读取修复说明.md
Mr.Xia b75439cff4 fix: 修复含特殊字符路径的钻带文件读取失败 — 移除 cmd /c type,改为 AnsiFileReader 直接文件IO
问题:路径含 ( ) & % ! ^ @ 等字符时报"文件路径包含不允许的命令行特殊字符",
(D2-f)、(1) 这类正常业务文件名直接命中。

根因:读取统一入口把路径拼进命令行(cmd.exe /d /c type "..."),为"防炸"在 03d9b8b
引入 ForbiddenPathChars 黑名单——并非业务需求,是实现的自我约束。
实测:真正会坏的只有 %VAR%(必坏) 与延迟展开开启时的 !VAR!,而 ( ) & ^ @ 空格在
双引号内均正常;且 type 是字节透传,这层 cmd 对当初的编码问题毫无帮助。

改动:
- 新增 AnsiFileReader.cs:FileStream + StreamReader(默认 936 解码 + BOM 探测,
  与旧 StreamReader 解码路径逐字符等价),不经命令行
- 删除 CommandTypeFileReader.cs 及其 ForbiddenPathChars 字符校验
- 路径只保留"非空"与"文件存在"校验;IO 异常包装为
  InvalidOperationException("读取文件失败:{路径}({原因})")并保留 InnerException
- 15 处调用点同步改名;更新 CLAUDE.md 与 Docs 说明

验证:14 个 demo_drl 样例新旧实现字符串/行数逐字符一致;含特殊字符路径(含 %TEMP% 字面
路径,旧实现 rc=1)可读可解析;UTF-8/UTF-16 BOM 结果与旧实现一致;
dotnet build -p:Platform=x86 0 错误且警告集与基线逐条相同。
2026-09-20 11:30:04 +08:00

5.8 KiB
Raw Blame History

含特殊字符路径的钻带文件读取修复说明

修复日期:2026-09-20(任务 .trellis/tasks/09-20-fix-special-char-path-read) 影响面:所有钻带/参数文件读取入口(共 15 处调用)

问题描述

选择路径或文件名中含特定字符的钻带/参数文件时,加载直接失败:

加载钻带文件失败:文件路径包含不允许的命令行特殊字符(Parameter 'filePath')

命中字符包括 ( ) & % ! ^ @ | < > 等。实际业务文件名很容易命中,例如:

  • 1009B-2013钻机参数表(D2-f)(A1A2厂) (1).doc
  • demo_drl (1)/xx.drl

问题分析

直接原因

读取统一入口 CommandTypeFileReader.ReadAllText 把文件路径拼进命令行,用子进程读文件:

FileName = "cmd.exe",
Arguments = $"/d /c type \"{filePath}\"",

因为路径会成为命令行的组成部分,代码在 ValidateAndNormalizePath 里加了一串字符黑名单 (ForbiddenPathChars)“防炸”,命中即抛异常。

历史成因(git 证据)

提交 内容
44de1cb(2025-12-17) “优化 sort.txt 读取方式…改用 cmd type 命令读取…提升编码兼容性”。真实动因是编码:.NET 6 下 Encoding.Default = UTF-8,读 GB2312 文件乱码,而当时 Encoding.GetEncoding(936) 因未注册 CodePages provider 会抛异常,于是绕道 cmd type + StandardOutputEncoding = 936
473b2ca(2026-05-23) 注册 CodePagesEncodingProvider,删掉 GetEncoding 的 catch 回退 —— 真正的根因已修,但没拆掉多余的 cmd 层
03d9b8b(2026-05-16,Refactor) 把散落的内联 ProcessStartInfo 抽成 CommandTypeFileReader,首次引入 ForbiddenPathChars 黑名单

即:把路径拼进命令行 ⇒ 必须“防炸” ⇒ 造出黑名单。黑名单并非业务需求,是实现的自我约束。

实测结论(Windows 10.0.26100,cmd /d /c type "<path>")

路径中的字符 实测结果 说明
( ) & ^ @ 空格 可正常读取 双引号内 cmd 不把 &/^ 当分隔符
%CD% / %TEMP% / %USERNAME% 读取失败(rc=1) 变量展开在双引号内依然发生
!CD! 默认可读;/v:on 时失败 依赖延迟展开开关
含 0x1A 等二进制字节 字节完全一致 type 是字节透传,不做任何转换

结论:黑名单属于过度防御 —— 真正会坏的只有 %VAR%(必坏)和 !VAR!(延迟展开开启时坏), 却把 ( ) & ^ @ 等完全安全的字符一并禁掉,直接误伤 (D2-f)、(1) 这类正常文件名。 而 type 既然只是字节透传,这层 cmd 对编码问题其实毫无贡献。

解决方案

  1. 新增 AnsiFileReader.cs(原 CommandTypeFileReader.cs 已删除),改为直接文件 IO:
using var stream = new FileStream(normalizedPath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true);
return reader.ReadToEnd();

detectEncodingFromByteOrderMarks: true 是为了保持旧行为:原先经 Process.StandardOutput(内部是 StreamReader)解码时默认会探测 BOM,所以带 UTF-8/UTF-16 BOM 的文件仍然按 BOM 指定的编码解码。

  1. 删除 ForbiddenPathChars 与两处字符校验;路径只保留“非空”与“文件存在”两项校验。
  2. IO 异常统一包装为可诊断消息:InvalidOperationException("读取文件失败:{路径}({原因})"),并保留 innerException。
  3. 默认编码仍为 ANSI/GB2312(code page 936),ReadAllText(filePath, encoding) 重载能力不变。
  4. 同步更新 .trellis/spec/backend/ 中 6 处已失效的规范描述(cmd type、危险字符校验、进程退出码)。

验证结果

用真实 demo_drl/ 样例与旧实现对照(同一编码解码):

验证项 结果
14 个 demo 样例:新旧实现字符串逐字符一致、行数一致 ✅ PASS
特殊字符路径可读:demo (a&b) 100%/xx (1) -A&B.drl、yy (2)!^.drl ✅ PASS
%VAR% 字面路径可读:pct%TEMP%end/pct%TEMP%end (3).drl(旧实现 rc=1 失败) ✅ PASS
BOM 行为一致:UTF-8 BOM / UTF-16 LE / UTF-16 BE 文件新旧实现结果相同 ✅ PASS
文件被独占占用 → InvalidOperationException("读取文件失败:...") 且 InnerException is IOException ✅ PASS
空路径 → ArgumentException("文件路径不能为空") ✅ PASS
不存在路径 → FileNotFoundException("文件不存在:...") ✅ PASS
dotnet build -p:Platform=x86 0 错误,无新增警告

行为一致性说明(解码层)

本次只替换“怎么拿到字节”(去掉命令行),没有改变“怎么解码”:

场景 旧实现(cmd type + StreamReader) 新实现(FileStream + StreamReader)
无 BOM 的 GB2312/ANSI 文件 936 解码 936 解码(一致)
UTF-8 BOM 文件 探测到 BOM → 改用 UTF-8、去掉 BOM 相同(一致)
UTF-16 LE/BE BOM 文件 探测到 BOM → 按 UTF-16 解码 相同(一致)
含 0x1A 等二进制字节 字节透传后 936 解码 相同(一致)

即:保留历史实现隐式具备的 BOM 支持,而不是新增特性,也不是回归。 若将来要改成固定 936(不探测 BOM),属于行为变更,需要单独任务与用户确认。

后续约定

  • 钻带/参数文件读取统一走 AnsiFileReader.ReadAllText,不要再引入子进程/命令行方式读文件。
  • 不要对文件路径做字符黑名单校验:读取不经过命令行,此类限制只会误伤正常文件名。
  • 需要更换编码时使用 ReadAllText(filePath, encoding) 重载,不要另起第二套读取逻辑。