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

224 lines
9.1 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.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();
}
}
}