# 含特殊字符路径的钻带文件读取修复说明 > 修复日期:2026-09-20(任务 `.trellis/tasks/09-20-restore-cmd-type-reader`) > 影响面:所有钻带/参数文件读取入口(共 15 处调用) > 本文件替换了同日先前一版的错误结论(“不存在加密层 / 可不经命令行”),见下文“上一轮的错误结论”。 ## 问题描述 选择路径或文件名中含特定字符的钻带/参数文件时,加载直接失败: ``` 加载钻带文件失败:文件路径包含不允许的命令行特殊字符(Parameter 'filePath') ``` 命中字符包括 `(` `)` `&` `%` `!` `^` `@` `|` `<` `>` 等。实际业务文件名很容易命中,例如: - `1009B-2013钻机参数表(D2-f)(A1A2厂) (1).doc` - `demo_drl (1)/xx.drl` ## 根因 读取统一入口 `CommandTypeFileReader.ReadAllText` 通过 `cmd.exe /d /c type ""` 读取文件: ```csharp 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 type` to 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”写成硬约束,避免再次被“优化掉”。 ## 本次改动 1. 恢复 `CommandTypeFileReader.cs`(`git show 2335894:CommandTypeFileReader.cs` 为基线), 读取仍为 `cmd.exe /d /c type ""`,`StandardOutputEncoding`/`StandardErrorEncoding` 仍为 ANSI/936; 失败时保留退出码与 stderr:`InvalidOperationException("读取文件失败,退出代码:{code},错误:{stderr}")`。 2. 删除 `ForbiddenPathChars` 与 `ValidateAndNormalizePath` 中两处字符校验(原始路径一次、规范化路径一次)。 路径只保留“非空”`ArgumentException("文件路径不能为空")` 与“文件存在”`FileNotFoundException("文件不存在:...")`。 3. 删除 `b75439c` 引入的 `AnsiFileReader.cs`;15 处调用点由 `AnsiFileReader.ReadAllText` 改回 `CommandTypeFileReader.ReadAllText`。 4. 保留 `ReadAllText(filePath, encoding)` 重载;默认编码仍为 ANSI/GB2312(code page 936)。 5. 修正 `CLAUDE.md`、`.trellis/spec/backend/`、`.trellis/spec/guides/index.md` 中上一轮被改错的描述。 ## 实测结论(Windows 10.0.26100,`cmd /d /c type ""`) | 路径中的字符 | 实测结果 | 说明 | |---|---|---| | `(` `)` `&` `^` `@` 空格 | **可正常读取** | 双引号内 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)` 重载,不要另起第二套读取逻辑。