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 基线完全一致。
97 lines
3.9 KiB
C#
97 lines
3.9 KiB
C#
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>( ) & ^ @</c> 与空格对 <c>type</c> 都是安全的
|
||
/// (双引号内 cmd 不把 <c>&</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);
|
||
}
|
||
}
|
||
}
|