fix: bind p-rec-annot line edit apply permits

This commit is contained in:
codex
2026-08-10 14:44:23 +03:00
parent 87887ffc76
commit eaa6044682
5 changed files with 122 additions and 7 deletions
@@ -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" }]));
}
}
@@ -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]