using System; using System.Diagnostics; using System.IO; using System.Text; namespace DrillTools { /// /// 钻带/参数文件统一读取入口。 /// /// 为什么必须走 cmd.exe /d /c type:项目存在部分“加密”的 txt/钻带文件, /// 无法用标准文件 I/O(File.ReadAllText / FileStream / StreamReader)直接读取, /// 只能通过 type 命令读出明文。历史上曾有提交(b75439c,2026-09-20) /// 以“命令行是多余的”为由改成直接文件 I/O,导致这些加密文件读取失败,已被回退。 /// 参见 CLAUDE.md 的 “Encrypted file reading” 与 /// .trellis/spec/backend/database-guidelines.md。 /// /// /// 为什么不做路径字符黑名单:路径被双引号包裹后,( ) & ^ @ 与空格对 type 都是安全的 /// (双引号内 cmd 不把 &/^ 当分隔符),而黑名单会误伤 /// (D2-f)、(1) 这类正常业务文件名。因此这里只校验“非空”与“文件存在”。 /// /// /// 已知残留限制(cmd 语义,不拦截):路径中出现 %VAR% 形状片段时会发生变量展开 /// (双引号内同样展开)导致读取失败;路径中的 ! 仅在延迟展开开启(/v:on 或注册表 /// DelayedExpansion=1)时才会失败。这类片段无法通过字符黑名单区分,故不做校验。 /// /// 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); } } }