fix: bind p-rec-annot line edit apply permits
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces the canonical, fixture-bound payload for the legacy line-number
|
||||||
|
/// editing commands. The command-specific Apply permit signs this value so a
|
||||||
|
/// controlled session cannot turn an arbitrary selected block or prompted
|
||||||
|
/// value into a write merely by switching runtime mode to Apply.
|
||||||
|
/// </summary>
|
||||||
|
public static class LineNumberEditApplyPlan
|
||||||
|
{
|
||||||
|
public static string ComputeMutationHash(string commandName, string selectedObjectHandle, IReadOnlyList<LineNumberRowMutation> mutations)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(commandName)) throw new ArgumentException("LINE_NUMBER_EDIT_COMMAND_REQUIRED", nameof(commandName));
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedObjectHandle)) throw new ArgumentException("LINE_NUMBER_EDIT_SELECTION_REQUIRED", nameof(selectedObjectHandle));
|
||||||
|
ArgumentNullException.ThrowIfNull(mutations);
|
||||||
|
if (mutations.Count == 0) throw new ArgumentException("LINE_NUMBER_EDIT_MUTATIONS_REQUIRED", nameof(mutations));
|
||||||
|
|
||||||
|
var canonical = new StringBuilder()
|
||||||
|
.Append(commandName).Append('|')
|
||||||
|
.Append(selectedObjectHandle).Append('\n');
|
||||||
|
foreach (var mutation in mutations.OrderBy(value => value.SourceStableId, StringComparer.Ordinal)
|
||||||
|
.ThenBy(value => value.PreviousMarkerNumber)
|
||||||
|
.ThenBy(value => value.MarkerNumber))
|
||||||
|
{
|
||||||
|
canonical.Append(mutation.SourceStableId).Append('|')
|
||||||
|
.Append(mutation.PreviousMarkerNumber).Append('|')
|
||||||
|
.Append(mutation.MarkerNumber).Append('|')
|
||||||
|
.Append(mutation.Text.Replace("\r\n", "\n", StringComparison.Ordinal).Replace("\r", "\n", StringComparison.Ordinal)).Append('|');
|
||||||
|
if (mutation.MarkerAttributePosition is { } position)
|
||||||
|
canonical.Append(position.X.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append(',')
|
||||||
|
.Append(position.Y.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append(',')
|
||||||
|
.Append(position.Z.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
canonical.Append('\n');
|
||||||
|
}
|
||||||
|
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Autodesk.AutoCAD.ApplicationServices;
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
using PRecAnnot.Abstractions;
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
namespace PRecAnnot.Plant3D2026;
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
@@ -12,6 +13,15 @@ internal sealed record LineNumberApplyPermit
|
|||||||
public required string ExpectedText { get; init; }
|
public required string ExpectedText { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed record LineNumberEditApplyPermit
|
||||||
|
{
|
||||||
|
public required bool AllowApply { get; init; }
|
||||||
|
public required string FixtureMarkerPath { get; init; }
|
||||||
|
public required string ExpectedCommandName { get; init; }
|
||||||
|
public required string ExpectedSelectedObjectHandle { get; init; }
|
||||||
|
public required string ExpectedMutationHash { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
internal static class LineNumberApplyPermitReader
|
internal static class LineNumberApplyPermitReader
|
||||||
{
|
{
|
||||||
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=LINE_NUMBER_APPLY";
|
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=LINE_NUMBER_APPLY";
|
||||||
@@ -35,3 +45,31 @@ internal static class LineNumberApplyPermitReader
|
|||||||
?? throw new InvalidOperationException("LINE_NUMBER_APPLY_PERMIT_INVALID");
|
?? throw new InvalidOperationException("LINE_NUMBER_APPLY_PERMIT_INVALID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static class LineNumberEditApplyPermitReader
|
||||||
|
{
|
||||||
|
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=LINE_NUMBER_EDIT_APPLY";
|
||||||
|
|
||||||
|
public static LineNumberEditApplyPermit Read()
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_EDIT_APPLY_PERMIT_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_PERMIT_REQUIRED");
|
||||||
|
return JsonSerializer.Deserialize<LineNumberEditApplyPermit>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||||
|
?? throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_PERMIT_INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Validate(Document document, LineNumberEditApplyPermit permit, string commandName, string selectedObjectHandle, IReadOnlyList<LineNumberRowMutation> mutations)
|
||||||
|
{
|
||||||
|
var mutationHash = LineNumberEditApplyPlan.ComputeMutationHash(commandName, selectedObjectHandle, mutations);
|
||||||
|
if (!permit.AllowApply ||
|
||||||
|
!string.Equals(permit.ExpectedCommandName, commandName, StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(permit.ExpectedSelectedObjectHandle, selectedObjectHandle, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!string.Equals(permit.ExpectedMutationHash, mutationHash, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_PERMIT_PLAN_MISMATCH");
|
||||||
|
if (!Path.IsPathFullyQualified(permit.FixtureMarkerPath) || permit.FixtureMarkerPath.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(permit.FixtureMarkerPath) || !string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), MarkerContent, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
var fixtureRoot = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))! + Path.DirectorySeparatorChar;
|
||||||
|
if (!Path.GetFullPath(document.Name).StartsWith(fixtureRoot, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ internal static class LineNumberEditCommands
|
|||||||
var start = editor.GetInteger(new PromptIntegerOptions("\nВведите новое начальное значение маркера: ") { LowerLimit = 1 });
|
var start = editor.GetInteger(new PromptIntegerOptions("\nВведите новое начальное значение маркера: ") { LowerLimit = 1 });
|
||||||
if (start.Status != PromptStatus.OK) return null;
|
if (start.Status != PromptStatus.OK) return null;
|
||||||
var rows = new LineNumberOwnedAnnotationReader().ReadList(document, selected);
|
var rows = new LineNumberOwnedAnnotationReader().ReadList(document, selected);
|
||||||
return new LineNumberAnnotationWriter().ApplyEdits(document, LineNumberEditPlanner.Renumber(rows, start.Value));
|
return LineNumberEditPlanner.Renumber(rows, start.Value);
|
||||||
}, "Выберите блок списка номеров линий: ");
|
}, "Выберите блок списка номеров линий: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ internal static class LineNumberEditCommands
|
|||||||
var row = rows.SingleOrDefault(value => value.MarkerNumber == marker.Value) ?? throw new InvalidOperationException("LINE_NUMBER_ROW_NOT_FOUND");
|
var row = rows.SingleOrDefault(value => value.MarkerNumber == marker.Value) ?? throw new InvalidOperationException("LINE_NUMBER_ROW_NOT_FOUND");
|
||||||
var text = editor.GetString(new PromptStringOptions("\nВведите новый текст номера линии: ") { AllowSpaces = true });
|
var text = editor.GetString(new PromptStringOptions("\nВведите новый текст номера линии: ") { AllowSpaces = true });
|
||||||
if (text.Status != PromptStatus.OK) return null;
|
if (text.Status != PromptStatus.OK) return null;
|
||||||
return new LineNumberAnnotationWriter().ApplyEdits(document, [LineNumberEditPlanner.EditText(row, text.StringResult)]);
|
return [LineNumberEditPlanner.EditText(row, text.StringResult)];
|
||||||
}, "Выберите блок списка номеров линий: ");
|
}, "Выберите блок списка номеров линий: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,11 +40,11 @@ internal static class LineNumberEditCommands
|
|||||||
{
|
{
|
||||||
var row = new LineNumberOwnedAnnotationReader().ReadMarker(document, selected);
|
var row = new LineNumberOwnedAnnotationReader().ReadMarker(document, selected);
|
||||||
var configured = LineNumberRuntimeConfigurationReader.Read().Assets.Marker.Texts[0].Position;
|
var configured = LineNumberRuntimeConfigurationReader.Read().Assets.Marker.Texts[0].Position;
|
||||||
return new LineNumberAnnotationWriter().ApplyEdits(document, [LineNumberEditPlanner.ResetMarkerAttributePosition(row, configured)]);
|
return [LineNumberEditPlanner.ResetMarkerAttributePosition(row, configured)];
|
||||||
}, "Выберите маркер номера линии: ");
|
}, "Выберите маркер номера линии: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Run(string command, Func<Document, Editor, ObjectId, LineNumberApplyResult?> operation, string prompt)
|
private static void Run(string command, Func<Document, Editor, ObjectId, IReadOnlyList<LineNumberRowMutation>?> operation, string prompt)
|
||||||
{
|
{
|
||||||
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
if (document is null) return;
|
if (document is null) return;
|
||||||
@@ -65,8 +65,11 @@ internal static class LineNumberEditCommands
|
|||||||
options.AddAllowedClass(typeof(BlockReference), true);
|
options.AddAllowedClass(typeof(BlockReference), true);
|
||||||
var selected = document.Editor.GetEntity(options);
|
var selected = document.Editor.GetEntity(options);
|
||||||
if (selected.Status != PromptStatus.OK) return;
|
if (selected.Status != PromptStatus.OK) return;
|
||||||
var result = operation(document, document.Editor, selected.ObjectId);
|
var mutations = operation(document, document.Editor, selected.ObjectId);
|
||||||
if (result is null) return;
|
if (mutations is null) return;
|
||||||
|
var selectedHandle = selected.ObjectId.Handle.ToString();
|
||||||
|
LineNumberEditApplyPermitReader.Validate(document, LineNumberEditApplyPermitReader.Read(), command, selectedHandle, mutations);
|
||||||
|
var result = new LineNumberAnnotationWriter().ApplyEdits(document, mutations);
|
||||||
document.Editor.WriteMessage($"\n{command}: updated={result.Updated}; unchanged={result.Unchanged}.");
|
document.Editor.WriteMessage($"\n{command}: updated={result.Updated}; unchanged={result.Unchanged}.");
|
||||||
RuntimeDiagnostics.Write("LineNumberEditCompleted", commandName: command, resultStatus: result.Updated > 0 ? "Updated" : "AlreadyCurrent", document: document,
|
RuntimeDiagnostics.Write("LineNumberEditCompleted", commandName: command, resultStatus: result.Updated > 0 ? "Updated" : "AlreadyCurrent", document: document,
|
||||||
details: new Dictionary<string, object?> { ["Updated"] = result.Updated, ["AlreadyCurrent"] = result.Unchanged });
|
details: new Dictionary<string, object?> { ["Updated"] = result.Updated, ["AlreadyCurrent"] = result.Unchanged });
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineNumberEditApplyPlanTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void MutationHash_IsStableAcrossInputOrder()
|
||||||
|
{
|
||||||
|
var first = new LineNumberRowMutation("pipe-a", 1, 10, "A", new(1, 2, 3));
|
||||||
|
var second = new LineNumberRowMutation("pipe-b", 2, 11, "B", null);
|
||||||
|
|
||||||
|
var expected = LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "1A2B", [first, second]);
|
||||||
|
|
||||||
|
Assert.Equal(expected, LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "1A2B", [second, first]));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MutationHash_BindsCommandSelectionAndRequestedEdit()
|
||||||
|
{
|
||||||
|
var mutation = new LineNumberRowMutation("pipe-a", 1, 10, "A", null);
|
||||||
|
var expected = LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "1A2B", [mutation]);
|
||||||
|
|
||||||
|
Assert.NotEqual(expected, LineNumberEditApplyPlan.ComputeMutationHash("PXP-ELN", "1A2B", [mutation]));
|
||||||
|
Assert.NotEqual(expected, LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "2B3C", [mutation]));
|
||||||
|
Assert.NotEqual(expected, LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "1A2B", [mutation with { Text = "Changed" }]));
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -39,7 +39,10 @@ public sealed class LineNumberEditCommandRegistrationTests
|
|||||||
public void MutationCommands_UseOneTransactionalWriter()
|
public void MutationCommands_UseOneTransactionalWriter()
|
||||||
{
|
{
|
||||||
var commands = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberEditCommands.cs"));
|
var commands = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberEditCommands.cs"));
|
||||||
Assert.Equal(3, System.Text.RegularExpressions.Regex.Matches(commands, "ApplyEdits\\(document").Count);
|
Assert.Single(System.Text.RegularExpressions.Regex.Matches(commands, "ApplyEdits\\(document").Cast<System.Text.RegularExpressions.Match>());
|
||||||
|
Assert.Contains("LineNumberEditApplyPermitReader.Validate", commands);
|
||||||
|
Assert.Contains("ExpectedSelectedObjectHandle", File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberApplyPermit.cs")));
|
||||||
|
Assert.Contains("ExpectedMutationHash", File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberApplyPermit.cs")));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user