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 Autodesk.AutoCAD.ApplicationServices;
|
||||
using PRecAnnot.Abstractions;
|
||||
using PRecAnnot.Core;
|
||||
|
||||
namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
@@ -12,6 +13,15 @@ internal sealed record LineNumberApplyPermit
|
||||
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
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
if (start.Status != PromptStatus.OK) return null;
|
||||
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 text = editor.GetString(new PromptStringOptions("\nВведите новый текст номера линии: ") { AllowSpaces = true });
|
||||
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 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;
|
||||
if (document is null) return;
|
||||
@@ -65,8 +65,11 @@ internal static class LineNumberEditCommands
|
||||
options.AddAllowedClass(typeof(BlockReference), true);
|
||||
var selected = document.Editor.GetEntity(options);
|
||||
if (selected.Status != PromptStatus.OK) return;
|
||||
var result = operation(document, document.Editor, selected.ObjectId);
|
||||
if (result is null) return;
|
||||
var mutations = operation(document, document.Editor, selected.ObjectId);
|
||||
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}.");
|
||||
RuntimeDiagnostics.Write("LineNumberEditCompleted", commandName: command, resultStatus: result.Updated > 0 ? "Updated" : "AlreadyCurrent", document: document,
|
||||
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()
|
||||
{
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user