Files
AohDrllTools/CommandTypeFileReader.cs
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

97 lines
3.9 KiB
C#
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.

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace DrillTools
{
/// <summary>
/// 钻带/参数文件统一读取入口。
/// <para>
/// <b>为什么必须走 <c>cmd.exe /d /c type</c></b>:项目存在部分“加密”的 txt/钻带文件,
/// 无法用标准文件 I/O(<c>File.ReadAllText</c> / <c>FileStream</c> / <c>StreamReader</c>)直接读取,
/// 只能通过 <c>type</c> 命令读出明文。历史上曾有提交(<c>b75439c</c>,2026-09-20)
/// 以“命令行是多余的”为由改成直接文件 I/O,导致这些加密文件读取失败,已被回退。
/// 参见 <c>CLAUDE.md</c> 的 “Encrypted file reading” 与
/// <c>.trellis/spec/backend/database-guidelines.md</c>。
/// </para>
/// <para>
/// <b>为什么不做路径字符黑名单</b>:路径被双引号包裹后,<c>( ) &amp; ^ @</c> 与空格对 <c>type</c> 都是安全的
/// (双引号内 cmd 不把 <c>&amp;</c>/<c>^</c> 当分隔符),而黑名单会误伤
/// <c>(D2-f)</c>、<c>(1)</c> 这类正常业务文件名。因此这里只校验“非空”与“文件存在”。
/// </para>
/// <para>
/// <b>已知残留限制(cmd 语义,不拦截)</b>:路径中出现 <c>%VAR%</c> 形状片段时会发生变量展开
/// (双引号内同样展开)导致读取失败;路径中的 <c>!</c> 仅在延迟展开开启(<c>/v:on</c> 或注册表
/// <c>DelayedExpansion=1</c>)时才会失败。这类片段无法通过字符黑名单区分,故不做校验。
/// </para>
/// </summary>
internal static class CommandTypeFileReader
{
private const int Gb2312CodePage = 936;
public static string ReadAllText(string filePath)
{
return ReadAllText(filePath, CreateAnsiEncoding());
}
public static string ReadAllText(string filePath, Encoding encoding)
{
string normalizedPath = ValidateAndNormalizePath(filePath);
using var process = new Process
{
StartInfo = CreateStartInfo(normalizedPath, encoding)
};
if (!process.Start())
throw new InvalidOperationException("无法启动文件读取进程");
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
process.WaitForExit();
string output = outputTask.GetAwaiter().GetResult();
string error = errorTask.GetAwaiter().GetResult();
if (process.ExitCode != 0)
throw new InvalidOperationException($"读取文件失败,退出代码:{process.ExitCode},错误:{error.Trim()}");
return output;
}
private static ProcessStartInfo CreateStartInfo(string filePath, Encoding encoding)
{
return new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/d /c type \"{filePath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = encoding,
StandardErrorEncoding = encoding
};
}
private static string ValidateAndNormalizePath(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("文件路径不能为空", nameof(filePath));
string normalizedPath = Path.GetFullPath(filePath);
if (!File.Exists(normalizedPath))
throw new FileNotFoundException($"文件不存在:{normalizedPath}", normalizedPath);
return normalizedPath;
}
private static Encoding CreateAnsiEncoding()
{
return Encoding.GetEncoding(Gb2312CodePage);
}
}
}