Files
AohDrllTools/DrlParameterReplacer.cs
Mr.Xia b75439cff4 fix: 修复含特殊字符路径的钻带文件读取失败 — 移除 cmd /c type,改为 AnsiFileReader 直接文件IO
问题:路径含 ( ) & % ! ^ @ 等字符时报"文件路径包含不允许的命令行特殊字符",
(D2-f)、(1) 这类正常业务文件名直接命中。

根因:读取统一入口把路径拼进命令行(cmd.exe /d /c type "..."),为"防炸"在 03d9b8b
引入 ForbiddenPathChars 黑名单——并非业务需求,是实现的自我约束。
实测:真正会坏的只有 %VAR%(必坏) 与延迟展开开启时的 !VAR!,而 ( ) & ^ @ 空格在
双引号内均正常;且 type 是字节透传,这层 cmd 对当初的编码问题毫无帮助。

改动:
- 新增 AnsiFileReader.cs:FileStream + StreamReader(默认 936 解码 + BOM 探测,
  与旧 StreamReader 解码路径逐字符等价),不经命令行
- 删除 CommandTypeFileReader.cs 及其 ForbiddenPathChars 字符校验
- 路径只保留"非空"与"文件存在"校验;IO 异常包装为
  InvalidOperationException("读取文件失败:{路径}({原因})")并保留 InnerException
- 15 处调用点同步改名;更新 CLAUDE.md 与 Docs 说明

验证:14 个 demo_drl 样例新旧实现字符串/行数逐字符一致;含特殊字符路径(含 %TEMP% 字面
路径,旧实现 rc=1)可读可解析;UTF-8/UTF-16 BOM 结果与旧实现一致;
dotnet build -p:Platform=x86 0 错误且警告集与基线逐条相同。
2026-09-20 11:30:04 +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 = AnsiFileReader.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 = AnsiFileReader.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;
}
}
}