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

108 lines
5.8 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-fix-special-char-path-read`)
> 影响面:所有钻带/参数文件读取入口(共 15 处调用)
## 问题描述
选择路径或文件名中含特定字符的钻带/参数文件时,加载直接失败:
```
加载钻带文件失败:文件路径包含不允许的命令行特殊字符(Parameter 'filePath')
```
命中字符包括 `(` `)` `&` `%` `!` `^` `@` `|` `<` `>` 等。实际业务文件名很容易命中,例如:
- `1009B-2013钻机参数表(D2-f)(A1A2厂) (1).doc`
- `demo_drl (1)/xx.drl`
## 问题分析
### 直接原因
读取统一入口 `CommandTypeFileReader.ReadAllText` 把文件路径拼进命令行,用子进程读文件:
```csharp
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:
```csharp
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 指定的编码解码。
2. 删除 `ForbiddenPathChars` 与两处字符校验;路径只保留“非空”与“文件存在”两项校验。
3. IO 异常统一包装为可诊断消息:`InvalidOperationException("读取文件失败:{路径}({原因})")`,并保留 `innerException`。
4. 默认编码仍为 ANSI/GB2312(code page 936),`ReadAllText(filePath, encoding)` 重载能力不变。
5. 同步更新 `.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)` 重载,不要另起第二套读取逻辑。