Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions src/AR.Iec61850/Scl/Export/LegacySasSclExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ public static LegacySasSclExportResult Build(
sourceName);

var document = new XDocument(filtered.Document);
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);

var retained = AssertSingleRetained(filtered);
var findings = normalized.Findings
Expand All @@ -100,7 +102,7 @@ public static LegacySasSclExportResult Build(
IedName = normalized.SelectedIedName,
AccessPointName = retained.AccessPointName,
SclSchema = schema.DisplayName,
RetainedReportControlReference = retained.DisplayReference,
RetainedReportControlReference = ExactRetainedReference(retained, options.SelectedReportControl),
RetainedDataSetName = retained.DataSetName,
RetainedDataSetMemberCount = retained.DataSetMemberCount,
RemovedReportControlCount = filtered.RemovedReportControlCount,
Expand Down Expand Up @@ -149,6 +151,63 @@ public static LegacySasSclExportResult WriteFiles(
return written;
}

private static void ApplyExactRuntimeReportControlIdentity(
XDocument document,
SclReportControlSelection selection)
{
var exactRuntimeName = (selection.ExportName ?? string.Empty).Trim();
if (exactRuntimeName.Length == 0)
return;

var reportControls = document.Descendants(Scl + "ReportControl").ToArray();
if (reportControls.Length != 1)
throw new InvalidDataException($"Exact runtime RCB normalization requires one retained ReportControl; found {reportControls.Length}.");

var retained = reportControls[0];
retained.SetAttributeValue("name", exactRuntimeName);
retained.SetAttributeValue("indexed", "false");

// ExportName already identifies the concrete MMS RCB instance. Keeping
// RptEnabled max=1 makes some legacy clients instantiate that exact name
// again and append another "01" (for example A_BRCB_1201 ->
// A_BRCB_120101). A non-indexed exact instance must therefore not carry
// the indexed-instantiation element in this legacy interoperability CID.
foreach (var rptEnabled in retained.Elements(Scl + "RptEnabled").ToArray())
rptEnabled.Remove();
}

private static void ValidateExactRuntimeReportControlIdentity(
XDocument document,
SclReportControlSelection selection)
{
var exactRuntimeName = (selection.ExportName ?? string.Empty).Trim();
if (exactRuntimeName.Length == 0)
return;

var retained = document.Descendants(Scl + "ReportControl").Single();
var actualName = (string?)retained.Attribute("name") ?? string.Empty;
if (!actualName.Equals(exactRuntimeName, StringComparison.Ordinal))
throw new InvalidDataException($"Filtered SCL changed exact runtime RCB name '{exactRuntimeName}' to '{actualName}'.");
if (!string.Equals((string?)retained.Attribute("indexed"), "false", StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException($"Exact runtime RCB '{exactRuntimeName}' must be exported as non-indexed.");
if (retained.Elements(Scl + "RptEnabled").Any())
throw new InvalidDataException($"Exact runtime RCB '{exactRuntimeName}' must not contain RptEnabled because that can append a second instance suffix.");
}

private static string ExactRetainedReference(
SclReportControlDescriptor retained,
SclReportControlSelection selection)
{
var exactRuntimeName = (selection.ExportName ?? string.Empty).Trim();
if (exactRuntimeName.Length == 0)
return retained.DisplayReference;

var separator = retained.DisplayReference.LastIndexOf('.');
return separator < 0
? exactRuntimeName
: retained.DisplayReference[..(separator + 1)] + exactRuntimeName;
}

private static SclReportControlDescriptor AssertSingleRetained(SclReportControlFilterResult filtered)
{
if (filtered.RetainedReportControls.Count != 1)
Expand Down Expand Up @@ -223,4 +282,4 @@ private static string BuildMarkdown(LegacySasSclExportResult result)
}
return builder.ToString();
}
}
}
30 changes: 29 additions & 1 deletion tests/AR.Iec61850.Tests/Scl/SclReportControlFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,34 @@ public void LegacyExporter_Applies_Edition1_Profile_And_Filter_With_Evidence()
Assert.DoesNotContain(result.Findings, finding => finding.Severity.Equals("Error", StringComparison.OrdinalIgnoreCase));
}

[Fact]
public void LegacyExporter_Preserves_Exact_Runtime_Rcb_Name_Without_Double_Indexing()
{
var source = XDocument.Parse(Fixture());
var descriptor = SclReportControlFilter.Inspect(source, "IED1.cid", "IED1", "AP1")
.ReportControls.Single(item => item.Name == "BRCB_EVENTS");

var result = LegacySasSclExporter.Build(
source,
"IED1.cid",
new LegacySasSclExportOptions
{
IedName = "IED1",
AccessPointName = "AP1",
SchemaProfile = SclSchemaProfile.Edition1V16,
SelectedReportControl = new SclReportControlSelection(
descriptor.SelectionKey,
"A_BRCB_1201")
});

var retained = Assert.Single(result.Document.Descendants(Scl + "ReportControl"));
Assert.Equal("A_BRCB_1201", (string?)retained.Attribute("name"));
Assert.Equal("false", (string?)retained.Attribute("indexed"));
Assert.Empty(retained.Elements(Scl + "RptEnabled"));
Assert.EndsWith(".A_BRCB_1201", result.RetainedReportControlReference, StringComparison.Ordinal);
Assert.DoesNotContain("A_BRCB_120101", result.Document.ToString(SaveOptions.DisableFormatting), StringComparison.Ordinal);
}

[Fact]
public void FilterLiveModel_Retains_One_Rcb_And_Validates_DataSet()
{
Expand Down Expand Up @@ -210,4 +238,4 @@ private static string Fixture()
<DataTypeTemplates><LNodeType id="LN0_TYPE" lnClass="LLN0" /></DataTypeTemplates>
</SCL>
""";
}
}
Loading