From fdc73f8e92fce3b67931a81d1706ad5dbfde95f2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 16 Sep 2026 16:54:02 +0700 Subject: [PATCH 1/2] fix(export): preserve source RCB identity on request --- src/AR.Iec61850/Scl/Export/LegacySasSclExporter.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/AR.Iec61850/Scl/Export/LegacySasSclExporter.cs b/src/AR.Iec61850/Scl/Export/LegacySasSclExporter.cs index ff4e9946..f9bd84ad 100644 --- a/src/AR.Iec61850/Scl/Export/LegacySasSclExporter.cs +++ b/src/AR.Iec61850/Scl/Export/LegacySasSclExporter.cs @@ -12,6 +12,7 @@ public sealed class LegacySasSclExportOptions public SclSchemaProfile SchemaProfile { get; init; } = SclSchemaProfile.Edition1V16; public SclReportControlSelection SelectedReportControl { get; init; } = new(string.Empty); public bool RemoveUnreferencedDataSets { get; init; } + public bool PreserveSourceReportControlIdentity { get; init; } public string ToolId { get; init; } = "ARIEC61850"; } @@ -70,17 +71,19 @@ public static LegacySasSclExportResult Build( SelectedReportControls = new[] { options.SelectedReportControl }, RequireExactlyOneReportControl = true, RemoveUnreferencedDataSets = options.RemoveUnreferencedDataSets, - CollapseIndexedSelectionToSingleInstance = true + CollapseIndexedSelectionToSingleInstance = !options.PreserveSourceReportControlIdentity }, sourceName); var document = new XDocument(filtered.Document); - ApplyExactRuntimeReportControlIdentity(document, options.SelectedReportControl); + if (!options.PreserveSourceReportControlIdentity) + ApplyExactRuntimeReportControlIdentity(document, options.SelectedReportControl); var root = document.Root ?? throw new InvalidDataException("Filtered SCL document has no root element."); var schema = SclSchemaProfiles.Get(options.SchemaProfile); ApplySchemaProfile(root, schema); Validate(document, normalized.SelectedIedName); - ValidateExactRuntimeReportControlIdentity(document, options.SelectedReportControl); + if (!options.PreserveSourceReportControlIdentity) + ValidateExactRuntimeReportControlIdentity(document, options.SelectedReportControl); var retained = AssertSingleRetained(filtered); var findings = normalized.Findings @@ -102,7 +105,9 @@ public static LegacySasSclExportResult Build( IedName = normalized.SelectedIedName, AccessPointName = retained.AccessPointName, SclSchema = schema.DisplayName, - RetainedReportControlReference = ExactRetainedReference(retained, options.SelectedReportControl), + RetainedReportControlReference = options.PreserveSourceReportControlIdentity + ? retained.DisplayReference + : ExactRetainedReference(retained, options.SelectedReportControl), RetainedDataSetName = retained.DataSetName, RetainedDataSetMemberCount = retained.DataSetMemberCount, RemovedReportControlCount = filtered.RemovedReportControlCount, From 9c316c2fa4d8d92321c496940d5f881cf9bbd736 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 16 Sep 2026 16:54:20 +0700 Subject: [PATCH 2/2] test(export): lock source-backed RCB identity --- .../Scl/LegacySasSourceIdentityExportTests.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/AR.Iec61850.Tests/Scl/LegacySasSourceIdentityExportTests.cs diff --git a/tests/AR.Iec61850.Tests/Scl/LegacySasSourceIdentityExportTests.cs b/tests/AR.Iec61850.Tests/Scl/LegacySasSourceIdentityExportTests.cs new file mode 100644 index 00000000..eb8fff12 --- /dev/null +++ b/tests/AR.Iec61850.Tests/Scl/LegacySasSourceIdentityExportTests.cs @@ -0,0 +1,80 @@ +using System.Xml.Linq; +using AR.Iec61850.Scl.Export; + +namespace AR.Iec61850.Tests.Scl; + +public sealed class LegacySasSourceIdentityExportTests +{ + private static readonly XNamespace Scl = "http://www.iec.ch/61850/2003/SCL"; + + [Fact] + public void SourceBackedExport_Preserves_Logical_Rcb_And_RptEnabled_Max() + { + var source = XDocument.Parse(Fixture()); + var descriptor = SclReportControlFilter.Inspect(source, "relay.cid", "AA1E1F06R4", "AP1") + .ReportControls.Single(); + + var result = LegacySasSclExporter.Build( + source, + "relay.cid", + new LegacySasSclExportOptions + { + IedName = "AA1E1F06R4", + AccessPointName = "AP1", + SelectedReportControl = new SclReportControlSelection(descriptor.SelectionKey, "Buffer02"), + PreserveSourceReportControlIdentity = true + }); + + var retained = Assert.Single(result.Document.Descendants(Scl + "ReportControl")); + Assert.Equal("Buffer", (string?)retained.Attribute("name")); + Assert.Equal("2", (string?)Assert.Single(retained.Elements(Scl + "RptEnabled")).Attribute("max")); + Assert.EndsWith(".Buffer", result.RetainedReportControlReference, StringComparison.Ordinal); + Assert.DoesNotContain("Buffer02", result.Document.ToString(SaveOptions.DisableFormatting), StringComparison.Ordinal); + } + + [Fact] + public void DefaultExport_Still_Uses_Exact_Runtime_Rcb_Name() + { + var source = XDocument.Parse(Fixture()); + var descriptor = SclReportControlFilter.Inspect(source, "relay.cid", "AA1E1F06R4", "AP1") + .ReportControls.Single(); + + var result = LegacySasSclExporter.Build( + source, + "relay.cid", + new LegacySasSclExportOptions + { + IedName = "AA1E1F06R4", + AccessPointName = "AP1", + SelectedReportControl = new SclReportControlSelection(descriptor.SelectionKey, "Buffer02") + }); + + var retained = Assert.Single(result.Document.Descendants(Scl + "ReportControl")); + Assert.Equal("Buffer02", (string?)retained.Attribute("name")); + Assert.Equal("false", (string?)retained.Attribute("indexed")); + Assert.Empty(retained.Elements(Scl + "RptEnabled")); + Assert.EndsWith(".Buffer02", result.RetainedReportControlReference, StringComparison.Ordinal); + } + + private static string Fixture() + => """ + + +
+ + + + + + + + + + + + + + + + """; +} \ No newline at end of file