Files
AohDrllTools/Docs/含特殊字符路径读取修复说明.md
Mr.Xia 209268ee18 fix: 还原 cmd /c type 读取并移除路径字符黑名单 — 加密钻带文件只能用 cmd type 读取
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 基线完全一致。
2026-09-20 13:28:37 +08:00

96 lines
5.7 KiB
Markdown
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.

# 含特殊字符路径的钻带文件读取修复说明
> 修复日期: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 "<path>"` 读取文件:
```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 "<path>"`,`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 "<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)` 重载,不要另起第二套读取逻辑。