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

276 lines
11 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>
/// DRL 钻带参数替换:将 A01-A14 参数文件中的 H/Z/S/F/U 参数替换到 .drl 文件的刀径定义行。
/// 与 ROU 不同,DRL 刀径行通常已有参数,此处为整体替换;对无参数段的刀径行则追加参数段。
/// </summary>
internal static class DrlParameterReplacer
{
private const int Gb2312CodePage = 936;
private const double DiameterTolerance = 0.001;
/// <summary>
/// 匹配参数文件行:T00C<直径>H<参数...>
/// 例:T00C0.749H02500Z+0.000S105.00F105.0U1000.0
/// </summary>
private static readonly Regex ParamLinePattern = new(
@"^T00C(?<diameter>\d+(?:\.\d+)?)(?<params>H.+)$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// 匹配 .drl 刀径定义行:T<刀号>C<直径>,参数段(H...)可选
/// 例:T01C0.799H05000Z+0.000S060.00F105.0U0700.0 → prefix="T01C0.799"
/// T01C0.799(无参数段)→ prefix="T01C0.799"
/// 匹配后统一重建为 prefix + 参数文件参数段:已有参数段整体替换,无参数段则追加。
/// </summary>
private static readonly Regex ToolLinePattern = new(
@"^\s*(?<prefix>T\d+C\d+(?:\.\d+)?)(?:H.+)?$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// 匹配 .drl 刀径行直径部分,用于提取直径数值
/// </summary>
private static readonly Regex ToolLineDiameterPattern = new(
@"^T\d+C(?<diameter>\d+(?:\.\d+)?)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// 匹配参数文件第一行(描述注释行):;<描述>
/// </summary>
private static readonly Regex DescriptionLinePattern = new(
@"^;(.+)$",
RegexOptions.Compiled);
/// <summary>
/// 执行参数替换。
/// </summary>
/// <param name="drlFilePath">.drl 文件完整路径</param>
/// <param name="paramFilePath">A01-A14 参数文件完整路径</param>
/// <param name="unmatchedDiameters">未在参数文件中找到的直径列表</param>
public static void ReplaceParameters(string drlFilePath, string paramFilePath, out List<double> unmatchedDiameters)
{
// 1. 读取参数文件,提取第一行描述和直径→参数字符串映射
string paramContent = CommandTypeFileReader.ReadAllText(paramFilePath);
var paramLines = DrillTapeLineParser.SplitLines(paramContent);
string paramDescription = ExtractDescription(paramLines);
if (string.IsNullOrEmpty(paramDescription))
throw new InvalidOperationException("参数文件第一行未找到有效的描述信息(格式:;<描述>)");
var paramDict = LoadParameterDictionary(paramLines);
if (paramDict.Count == 0)
throw new InvalidOperationException("参数文件中未找到有效的参数行(格式:T00C<直径>H...)");
// 2. 读取 .drl 文件
string drlContent = CommandTypeFileReader.ReadAllText(drlFilePath);
// 3. 执行替换(同时收集未匹配直径)
var unmatchedSet = new HashSet<double>();
string replacedContent = ReplaceParametersInContent(drlContent, paramDict, paramDescription, unmatchedSet);
// 4. 备份原文件
string backupFilePath = drlFilePath + ".bak";
File.Copy(drlFilePath, backupFilePath, true);
// 5. 写回文件
using var writer = new StreamWriter(drlFilePath, false, Encoding.GetEncoding(Gb2312CodePage));
writer.Write(replacedContent);
unmatchedDiameters = new List<double>(unmatchedSet);
}
/// <summary>
/// 从参数文件行中提取第一行描述(去掉前导分号)。
/// </summary>
private static string ExtractDescription(IReadOnlyList<DrillTapeLine> paramLines)
{
foreach (var line in paramLines)
{
string trimmed = line.Text.Trim();
var match = DescriptionLinePattern.Match(trimmed);
if (match.Success)
return match.Groups[1].Value.Trim();
}
return string.Empty;
}
/// <summary>
/// 从参数文件加载直径→参数字符串(从 H 开始到行尾)的映射。
/// </summary>
private static Dictionary<double, string> LoadParameterDictionary(IReadOnlyList<DrillTapeLine> paramLines)
{
var dict = new Dictionary<double, string>();
foreach (var line in paramLines)
{
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;
diameter = Math.Round(diameter, 3);
string paramString = match.Groups["params"].Value;
// 同一直径后面覆盖前面
dict[diameter] = paramString;
}
return dict;
}
/// <summary>
/// 在 .drl 文件内容中执行参数替换:
/// 1. M48 后一行若是注释行则替换为参数文件的描述;否则原样保留并在 M48 后插入描述行
/// 2. M48~% 区域内按直径匹配替换刀径行的参数段(无参数段的刀径行追加参数段)
/// </summary>
private static string ReplaceParametersInContent(
string drlContent,
Dictionary<double, string> paramDict,
string paramDescription,
HashSet<double> unmatchedDiameters)
{
var lines = DrillTapeLineParser.SplitLines(drlContent);
// 找到头部边界
int headerStart = -1; // M48 所在行
int headerEnd = -1; // % 所在行
for (int i = 0; i < lines.Count; i++)
{
string trimmed = lines[i].Text.Trim();
if (headerStart < 0 && trimmed == "M48")
headerStart = i;
else if (headerStart >= 0 && trimmed == "%")
{
headerEnd = i;
break;
}
}
if (headerStart < 0 || headerEnd < 0)
throw new InvalidOperationException("DRL 文件格式异常:未找到 M48 或 % 头部标记");
var builder = new StringBuilder(drlContent.Length + 1024);
for (int i = 0; i < lines.Count; i++)
{
if (i == headerStart + 1)
{
// M48 下一行:若是注释行则替换为参数文件的描述;
// 否则(如工具定义行)原样保留,并在其前(M48 后)插入描述行,不破坏数据;
// 该行本身仍按刀径行规则处理(替换/追加/未匹配)。
string trimmed = lines[i].Text.Trim();
if (trimmed.StartsWith(";"))
{
string trailing = GetTrailingWhitespace(lines[i]);
builder.Append(';');
builder.Append(paramDescription);
builder.Append(trailing);
builder.Append(lines[i].LineEnding);
}
else
{
builder.Append(';');
builder.Append(paramDescription);
builder.Append(lines[i].LineEnding);
AppendToolLine(builder, lines[i], trimmed, paramDict, unmatchedDiameters);
}
}
else if (i > headerStart && i < headerEnd)
{
// 头部区域(M48 和 % 之间,排除已处理的第2行)
string trimmed = lines[i].Text.Trim();
AppendToolLine(builder, lines[i], trimmed, paramDict, unmatchedDiameters);
}
else
{
// % 之后或其他区域原样保留
builder.Append(lines[i].Text);
builder.Append(lines[i].LineEnding);
}
}
return builder.ToString();
}
/// <summary>
/// 按刀径行规则追加一行:匹配直径则重建为 prefix + 参数文件参数段(
/// 已有参数段整体替换,无参数段则追加);未匹配或非刀径行保留原行,
/// 未匹配直径计入 unmatchedDiameters。
/// </summary>
private static void AppendToolLine(
StringBuilder builder,
DrillTapeLine line,
string trimmed,
Dictionary<double, string> paramDict,
HashSet<double> unmatchedDiameters)
{
var match = ToolLinePattern.Match(trimmed);
if (!match.Success)
{
// 非刀径行(如其他注释等)原样保留
builder.Append(line.Text);
builder.Append(line.LineEnding);
return;
}
string prefix = match.Groups["prefix"].Value;
string trailing = GetTrailingWhitespace(line);
// 从 prefix 中提取直径
var diaMatch = ToolLineDiameterPattern.Match(prefix);
if (!diaMatch.Success ||
!double.TryParse(diaMatch.Groups["diameter"].Value,
NumberStyles.Float, CultureInfo.InvariantCulture, out double diameter))
{
// 无法提取直径,保留原行
builder.Append(line.Text);
builder.Append(line.LineEnding);
return;
}
double roundedDiameter = Math.Round(diameter, 3);
if (paramDict.TryGetValue(roundedDiameter, out string? newParams))
{
builder.Append(prefix);
builder.Append(newParams);
builder.Append(trailing);
builder.Append(line.LineEnding);
}
else
{
// 未匹配:保留原行
unmatchedDiameters.Add(roundedDiameter);
builder.Append(line.Text);
builder.Append(line.LineEnding);
}
}
/// <summary>
/// 获取行尾空白字符。
/// </summary>
private static string GetTrailingWhitespace(DrillTapeLine line)
{
string original = line.Text;
int trimmedLength = original.TrimEnd().Length;
return trimmedLength < original.Length ? original[trimmedLength..] : string.Empty;
}
}
}