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 基线完全一致。
224 lines
9.1 KiB
C#
224 lines
9.1 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
|
||
namespace DrillTools
|
||
{
|
||
/// <summary>
|
||
/// ROU 成型参数替换:将参数文件中的 S/F/R/Z/H 参数追加到 .rou 文件的刀径定义行。
|
||
/// </summary>
|
||
internal static class RouParameterReplacer
|
||
{
|
||
private const int Gb2312CodePage = 936;
|
||
private const double DiameterTolerance = 0.001;
|
||
|
||
/// <summary>
|
||
/// 匹配参数行:T00C<直径>S<参数...>
|
||
/// 例:T00C1.800S55.0F5.0R25Z0H600
|
||
/// </summary>
|
||
private static readonly Regex ParamLinePattern = new(
|
||
@"^T00C(?<diameter>\d+(?:\.\d+)?)(?<params>S.+?)\s*$",
|
||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||
|
||
/// <summary>
|
||
/// 匹配 .rou 刀径定义行:T<刀号>C<直径>
|
||
/// 例:T01C1.8 → prefix="T01C", diameter="1.8"
|
||
/// </summary>
|
||
private static readonly Regex ToolLinePattern = new(
|
||
@"^(?<prefix>T\d+C)(?<diameter>\d+(?:\.\d+)?)",
|
||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||
|
||
/// <summary>
|
||
/// 执行参数替换。
|
||
/// </summary>
|
||
/// <param name="rouFilePath">.rou 文件完整路径</param>
|
||
/// <param name="paramFilePath">参数 .txt 文件完整路径</param>
|
||
public static void ReplaceParameters(string rouFilePath, string paramFilePath, out List<double> unmatchedDiameters)
|
||
{
|
||
// 1. 读取参数文件,构建直径→参数字符串映射
|
||
var paramDict = LoadParameterDictionary(paramFilePath);
|
||
|
||
if (paramDict.Count == 0)
|
||
throw new InvalidOperationException("参数文件中未找到有效的参数行(格式:T00C<直径>S...)");
|
||
|
||
// 2. 读取 .rou 文件
|
||
string rouContent = CommandTypeFileReader.ReadAllText(rouFilePath);
|
||
|
||
// 3. 执行替换(同时收集未匹配直径)
|
||
var unmatchedSet = new HashSet<double>();
|
||
string replacedContent = ReplaceParametersInContent(rouContent, paramDict, unmatchedSet);
|
||
|
||
// 4. 备份原文件
|
||
string backupFilePath = rouFilePath + ".bak";
|
||
File.Copy(rouFilePath, backupFilePath, true);
|
||
|
||
// 5. 写回文件
|
||
using var writer = new StreamWriter(rouFilePath, false, Encoding.GetEncoding(Gb2312CodePage));
|
||
writer.Write(replacedContent);
|
||
|
||
// 6. 输出未匹配直径
|
||
unmatchedDiameters = new List<double>(unmatchedSet);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从参数文件加载直径→参数字符串(从 S 开始到行尾)的映射。
|
||
/// </summary>
|
||
private static Dictionary<double, string> LoadParameterDictionary(string paramFilePath)
|
||
{
|
||
var dict = new Dictionary<double, string>();
|
||
|
||
string content = CommandTypeFileReader.ReadAllText(paramFilePath);
|
||
var lines = DrillTapeLineParser.SplitLines(content);
|
||
|
||
foreach (var line in lines)
|
||
{
|
||
string trimmed = line.Text.Trim();
|
||
var match = ParamLinePattern.Match(trimmed);
|
||
if (!match.Success)
|
||
continue;
|
||
|
||
if (!double.TryParse(match.Groups["diameter"].Value,
|
||
NumberStyles.Float, CultureInfo.InvariantCulture, out double diameter))
|
||
continue;
|
||
|
||
// 规范化到3位小数,消除浮点精度差异
|
||
diameter = Math.Round(diameter, 3);
|
||
|
||
string paramString = match.Groups["params"].Value;
|
||
|
||
// 如果同一直径出现多次,后面的覆盖前面的
|
||
dict[diameter] = paramString;
|
||
}
|
||
|
||
return dict;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在 .rou 文件内容中,将 M48~% 区域内的刀径行末尾追加参数。
|
||
/// 使用两遍扫描:第一遍按基准直径分组并从参数字典中分配可用键,第二遍执行替换。
|
||
/// </summary>
|
||
private static string ReplaceParametersInContent(string rouContent, Dictionary<double, string> paramDict, HashSet<double> unmatchedDiameters)
|
||
{
|
||
var lines = DrillTapeLineParser.SplitLines(rouContent);
|
||
|
||
// === Pass 1: 收集头部区域所有刀径行,按基准直径分组,分配参数键 ===
|
||
bool inHeader = false;
|
||
var toolEntries = new List<(int Index, string Prefix, double Diameter)>();
|
||
|
||
for (int i = 0; i < lines.Count; i++)
|
||
{
|
||
string trimmed = lines[i].Text.Trim();
|
||
|
||
if (!inHeader && trimmed == "M48")
|
||
{
|
||
inHeader = true;
|
||
continue;
|
||
}
|
||
|
||
if (inHeader && trimmed == "%")
|
||
{
|
||
inHeader = false;
|
||
continue;
|
||
}
|
||
|
||
if (inHeader)
|
||
{
|
||
var match = ToolLinePattern.Match(trimmed);
|
||
if (match.Success &&
|
||
double.TryParse(match.Groups["diameter"].Value,
|
||
NumberStyles.Float, CultureInfo.InvariantCulture, out double diameter))
|
||
{
|
||
diameter = Math.Round(diameter, 3);
|
||
string prefix = match.Groups["prefix"].Value;
|
||
toolEntries.Add((i, prefix, diameter));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 按基准直径分组,为每组分配显示直径和参数键
|
||
// lineIndex -> (prefix, displayDiameter, assignedParamKey | null)
|
||
var toolMap = new Dictionary<int, (string Prefix, double DisplayDiameter, double? ParamKey)>();
|
||
|
||
foreach (var group in toolEntries.GroupBy(e => e.Diameter))
|
||
{
|
||
double baseDiam = group.Key;
|
||
var occurrences = group.ToList();
|
||
|
||
// 在 [baseDiam, baseDiam + 0.01) 范围内查找候选参数键
|
||
var candidates = paramDict.Keys
|
||
.Where(k => k >= baseDiam && k < baseDiam + 0.01)
|
||
.OrderBy(k => k)
|
||
.ToList();
|
||
|
||
// 如果没有候选键,尝试容差回退(仅对首个出现)
|
||
if (candidates.Count == 0)
|
||
{
|
||
foreach (var kvp in paramDict)
|
||
{
|
||
if (Math.Abs(kvp.Key - baseDiam) < DiameterTolerance)
|
||
{
|
||
candidates.Add(kvp.Key);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分配:每个出现的显示直径为 baseDiam + j*0.001(3位小数补零)
|
||
// 参数键从候选列表中按序取,超出则为 null(未匹配)
|
||
for (int j = 0; j < occurrences.Count; j++)
|
||
{
|
||
double displayDiameter = Math.Round(baseDiam + j * 0.001, 3);
|
||
double? assignedKey = j < candidates.Count ? candidates[j] : null;
|
||
toolMap[occurrences[j].Index] = (occurrences[j].Prefix, displayDiameter, assignedKey);
|
||
}
|
||
}
|
||
|
||
// === Pass 2: 执行替换(修改直径文本 + 追加参数) ===
|
||
var builder = new StringBuilder(rouContent.Length + 1024);
|
||
|
||
for (int i = 0; i < lines.Count; i++)
|
||
{
|
||
if (toolMap.TryGetValue(i, out var toolInfo))
|
||
{
|
||
if (toolInfo.ParamKey.HasValue)
|
||
{
|
||
string paramString = paramDict[toolInfo.ParamKey.Value];
|
||
|
||
// 重建行:prefix + 显示直径 + 参数
|
||
string displayDiam = toolInfo.DisplayDiameter.ToString("F3", CultureInfo.InvariantCulture);
|
||
|
||
// 保留行尾空白
|
||
string originalLine = lines[i].Text.TrimEnd();
|
||
int trailingLength = lines[i].Text.Length - originalLine.Length;
|
||
string trailing = trailingLength > 0 ? lines[i].Text[^trailingLength..] : string.Empty;
|
||
|
||
builder.Append(toolInfo.Prefix);
|
||
builder.Append(displayDiam);
|
||
builder.Append(paramString);
|
||
builder.Append(trailing);
|
||
builder.Append(lines[i].LineEnding);
|
||
}
|
||
else
|
||
{
|
||
// 未匹配:保留原始行,记录直径
|
||
unmatchedDiameters.Add(toolInfo.DisplayDiameter);
|
||
builder.Append(lines[i].Text);
|
||
builder.Append(lines[i].LineEnding);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 非刀径行原样保留
|
||
builder.Append(lines[i].Text);
|
||
builder.Append(lines[i].LineEnding);
|
||
}
|
||
}
|
||
|
||
return builder.ToString();
|
||
}
|
||
}
|
||
}
|