From ea8bfa8576ac35f1198f250c1e35354103295e76 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:42:29 +0700 Subject: [PATCH 1/4] fix(browser): fence raw Report quality by exact optional DataRef --- Services/IedBrowserReportWireEvidence.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Services/IedBrowserReportWireEvidence.cs b/Services/IedBrowserReportWireEvidence.cs index 10eb1aeaa..6c5bc25bd 100644 --- a/Services/IedBrowserReportWireEvidence.cs +++ b/Services/IedBrowserReportWireEvidence.cs @@ -21,6 +21,16 @@ public static IReadOnlyList Extract(MmsReportFra { var reference = IedBrowserLiveValues.CanonicalReference(reported.MemberReference); var fc = reported.Member?.FunctionalConstraint ?? ""; + // When OptFlds contains DataRef, its on-wire identity must agree + // with the resolved DataSet member before any raw quality/time + // evidence is bound to a Browser DO. Absence of optional DataRef + // preserves the existing engine-verified member/index route. + // Never resolve a disagreement by short name or suffix. + if (!string.IsNullOrWhiteSpace(reported.DataReference) && + !string.Equals( + IedBrowserLiveValues.CanonicalReference(reported.DataReference), + reference, StringComparison.Ordinal)) + continue; var payload = reported.Value; if (reported.FailureCode.HasValue || payload is null || string.IsNullOrWhiteSpace(reference) || string.IsNullOrWhiteSpace(fc)) From 827203cb8997b57c7ee0c103401c907a891995e7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:42:32 +0700 Subject: [PATCH 2/4] test(browser): prove exact raw quality identity and mismatched DataRef rejection --- .../M214ReportQualityDataRefIdentityTests.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/ARSAS.Tests/M214ReportQualityDataRefIdentityTests.cs diff --git a/tests/ARSAS.Tests/M214ReportQualityDataRefIdentityTests.cs b/tests/ARSAS.Tests/M214ReportQualityDataRefIdentityTests.cs new file mode 100644 index 000000000..60e5d102c --- /dev/null +++ b/tests/ARSAS.Tests/M214ReportQualityDataRefIdentityTests.cs @@ -0,0 +1,79 @@ +using AR.Iec61850.Mms; +using ArIED61850Tester.Services; + +namespace ARSAS.Tests; + +public sealed class M214ReportQualityDataRefIdentityTests +{ + private const string QualityReference = "LD/CSWI1.Pos.q"; + + private static MmsReportFrame Frame( + string? dataReference, + string memberReference = QualityReference) + => new() + { + Values = + [ + new MmsReportValue + { + Index = 0, + Member = new MmsDataSetDirectoryMember + { + UserReference = memberReference, + FunctionalConstraint = "ST" + }, + DataReference = dataReference ?? "", + // IEC 61850 Quality: Overflow, OutOfRange, OldData and + // Inaccurate set, while Source remains process. + Value = MmsDataValue.BitString(3, new byte[] { 0x31, 0x40 }) + } + ] + }; + + [Theory] + [InlineData("LD/CSWI1.Pos.q")] + [InlineData("LD/CSWI1$ST$Pos$q")] + [InlineData(null)] + public void ExactOrAbsentOptionalDataRef_PreservesActualRawQuality(string? dataReference) + { + var raw = Assert.Single(IedBrowserReportWireEvidence.Extract(Frame(dataReference))); + Assert.Equal(QualityReference, raw.CarrierReference); + Assert.Equal("ST", raw.FunctionalConstraint); + var quality = Assert.IsType(raw.QualityBits); + Assert.Equal("true", quality.FormatField("Overflow")); + Assert.Equal("true", quality.FormatField("OutOfRange")); + Assert.Equal("true", quality.FormatField("OldData")); + Assert.Equal("true", quality.FormatField("Inaccurate")); + Assert.Equal("process", quality.FormatField("Source")); + } + + [Theory] + [InlineData("LD/CSWI1.Mod.q")] + [InlineData("OTHER/CSWI1.Pos.q")] + [InlineData("LD/CSWI2.Pos.q")] + [InlineData("LD/CSWI1$MX$Pos$q")] + public void ConflictingOnWireDataRef_CannotAttributeBitsToAnotherObject( + string contradictoryReference) + { + Assert.Empty(IedBrowserReportWireEvidence.Extract( + Frame(contradictoryReference))); + } + + [Fact] + public void MissingMemberIdentity_CannotBeRepairedFromOnWireText() + { + var frame = new MmsReportFrame + { + Values = + [ + new MmsReportValue + { + Index = 0, + DataReference = "LD/CSWI1$ST$Pos$q", + Value = MmsDataValue.BitString(3, new byte[] { 0x31, 0x40 }) + } + ] + }; + Assert.Empty(IedBrowserReportWireEvidence.Extract(frame)); + } +} From 4363dea8cd47d3ecda35decc43a1bb9605d20b1c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:43:20 +0700 Subject: [PATCH 3/4] fix(browser): reject cross-FC DataRef aliases before raw quality projection --- Services/IedBrowserReportWireEvidence.cs | 55 ++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/Services/IedBrowserReportWireEvidence.cs b/Services/IedBrowserReportWireEvidence.cs index 6c5bc25bd..c46981583 100644 --- a/Services/IedBrowserReportWireEvidence.cs +++ b/Services/IedBrowserReportWireEvidence.cs @@ -26,10 +26,7 @@ public static IReadOnlyList Extract(MmsReportFra // evidence is bound to a Browser DO. Absence of optional DataRef // preserves the existing engine-verified member/index route. // Never resolve a disagreement by short name or suffix. - if (!string.IsNullOrWhiteSpace(reported.DataReference) && - !string.Equals( - IedBrowserLiveValues.CanonicalReference(reported.DataReference), - reference, StringComparison.Ordinal)) + if (!DataReferenceAgreesWithMember(reported.DataReference, reference, fc)) continue; var payload = reported.Value; if (reported.FailureCode.HasValue || payload is null || @@ -67,6 +64,56 @@ public static IReadOnlyList Extract(MmsReportFra return result; } + + /// + /// Match both the IEC attribute path and, when the wire DataRef has an MMS + /// $FC$ component, its FC. CanonicalReference alone intentionally removes + /// the FC for ledger keys and therefore cannot prove this wire identity. + /// + private static bool DataReferenceAgreesWithMember( + string? dataReference, string memberReference, string memberFc) + { + if (string.IsNullOrWhiteSpace(dataReference)) + return true; // DataRef is optional; the decoded member remains authoritative. + var wire = dataReference.Trim(); + if (!string.Equals(IedBrowserLiveValues.CanonicalReference(wire), + memberReference, StringComparison.Ordinal)) + return false; + var fcStart = wire.IndexOf(' + => raw.Kind == MmsDataKind.BitString && + MmsQualityDecoder.TryDecode(raw.RawValue.ToArray(), out var decoded) + ? decoded : null; + + private static Iec61850UtcTimeEvidence? DecodeTime(MmsDataValue raw) + { + if (raw.Kind != MmsDataKind.UtcTime) return null; + var decoded = Iec61850UtcTimeEvidence.Decode(raw); + return decoded is { IsDecoded: true, HasWireProvenance: true } + ? decoded : null; + } +} +); + if (fcStart < 0) + return true; // Standard dotted IEC reference carries no FC segment. + var fcEnd = wire.IndexOf(' + => raw.Kind == MmsDataKind.BitString && + MmsQualityDecoder.TryDecode(raw.RawValue.ToArray(), out var decoded) + ? decoded : null; + + private static Iec61850UtcTimeEvidence? DecodeTime(MmsDataValue raw) + { + if (raw.Kind != MmsDataKind.UtcTime) return null; + var decoded = Iec61850UtcTimeEvidence.Decode(raw); + return decoded is { IsDecoded: true, HasWireProvenance: true } + ? decoded : null; + } +} +, fcStart + 1); + return fcEnd > fcStart + 1 && + string.Equals(wire[(fcStart + 1)..fcEnd], memberFc, + StringComparison.OrdinalIgnoreCase); + } + private static MmsQualityEvidence? DecodeQuality(MmsDataValue raw) => raw.Kind == MmsDataKind.BitString && MmsQualityDecoder.TryDecode(raw.RawValue.ToArray(), out var decoded) From 02ed1e3b33abd281e7d59513faaf1b11c4ef58ec Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:48:35 +0700 Subject: [PATCH 4/4] fix(browser): repair exact Report DataRef and FC fence compilation --- Services/IedBrowserReportWireEvidence.cs | 48 ++++-------------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/Services/IedBrowserReportWireEvidence.cs b/Services/IedBrowserReportWireEvidence.cs index c46981583..9b46d1701 100644 --- a/Services/IedBrowserReportWireEvidence.cs +++ b/Services/IedBrowserReportWireEvidence.cs @@ -21,11 +21,8 @@ public static IReadOnlyList Extract(MmsReportFra { var reference = IedBrowserLiveValues.CanonicalReference(reported.MemberReference); var fc = reported.Member?.FunctionalConstraint ?? ""; - // When OptFlds contains DataRef, its on-wire identity must agree - // with the resolved DataSet member before any raw quality/time - // evidence is bound to a Browser DO. Absence of optional DataRef - // preserves the existing engine-verified member/index route. - // Never resolve a disagreement by short name or suffix. + // DataRef is optional. If present it must agree with the verified + // DataSet member AND explicit FC, or the raw q/t is quarantined. if (!DataReferenceAgreesWithMember(reported.DataReference, reference, fc)) continue; var payload = reported.Value; @@ -65,50 +62,21 @@ public static IReadOnlyList Extract(MmsReportFra } - /// - /// Match both the IEC attribute path and, when the wire DataRef has an MMS - /// $FC$ component, its FC. CanonicalReference alone intentionally removes - /// the FC for ledger keys and therefore cannot prove this wire identity. - /// private static bool DataReferenceAgreesWithMember( string? dataReference, string memberReference, string memberFc) { if (string.IsNullOrWhiteSpace(dataReference)) - return true; // DataRef is optional; the decoded member remains authoritative. + return true; var wire = dataReference.Trim(); if (!string.Equals(IedBrowserLiveValues.CanonicalReference(wire), memberReference, StringComparison.Ordinal)) return false; - var fcStart = wire.IndexOf(' - => raw.Kind == MmsDataKind.BitString && - MmsQualityDecoder.TryDecode(raw.RawValue.ToArray(), out var decoded) - ? decoded : null; - - private static Iec61850UtcTimeEvidence? DecodeTime(MmsDataValue raw) - { - if (raw.Kind != MmsDataKind.UtcTime) return null; - var decoded = Iec61850UtcTimeEvidence.Decode(raw); - return decoded is { IsDecoded: true, HasWireProvenance: true } - ? decoded : null; - } -} -); + // The canonical ledger deliberately drops the MMS $FC$ segment. + // Recheck this separately to reject cross-FC alias collisions. + var fcStart = wire.IndexOf('$'); if (fcStart < 0) - return true; // Standard dotted IEC reference carries no FC segment. - var fcEnd = wire.IndexOf(' - => raw.Kind == MmsDataKind.BitString && - MmsQualityDecoder.TryDecode(raw.RawValue.ToArray(), out var decoded) - ? decoded : null; - - private static Iec61850UtcTimeEvidence? DecodeTime(MmsDataValue raw) - { - if (raw.Kind != MmsDataKind.UtcTime) return null; - var decoded = Iec61850UtcTimeEvidence.Decode(raw); - return decoded is { IsDecoded: true, HasWireProvenance: true } - ? decoded : null; - } -} -, fcStart + 1); + return true; + var fcEnd = wire.IndexOf('$', fcStart + 1); return fcEnd > fcStart + 1 && string.Equals(wire[(fcStart + 1)..fcEnd], memberFc, StringComparison.OrdinalIgnoreCase);