Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b3c2b45
feat(p0): port ARIEC61850 fail-closed exact SCL DataSet report binding
masarray Oct 17, 2026
3afe9e9
feat(p0): project model-verified report leaves with fail-closed posit…
masarray Oct 17, 2026
21433d4
feat(p0): bind report worker to generation-scoped canonical SCL seman…
masarray Oct 17, 2026
bb2a1e4
safety(p0): reject contradictory report references and ambiguous iden…
masarray Oct 17, 2026
8d420ec
test(p0): exercise exact SCL-bound report leaves and six fail-closed …
masarray Oct 17, 2026
e4f0e5b
ui(p0): distinguish model-bound semantic report leaves from positiona…
masarray Oct 17, 2026
f29e8ba
test(p0): guard authoritative SCL semantic projection and negative bi…
masarray Oct 17, 2026
307ae31
feat(p0): reuse canonical Quality/TimeQuality decoder for verified re…
masarray Oct 17, 2026
7e026b5
ui(p0): expand verified q/t Quality and TimeQuality beneath semantic …
masarray Oct 17, 2026
818f732
test(p0): verify typed quality/time evidence has native owner and inl…
masarray Oct 17, 2026
7c156c3
fix(p0): reject ambiguous SCL fields sharing compatible MMS wire type
masarray Oct 17, 2026
27b5223
fix(p0): bind missing optional DatSet from verified active RCB state
masarray Oct 17, 2026
c4cb126
test(p0): enforce MMS-kind ambiguity rejection and verified optional …
masarray Oct 17, 2026
27fbd85
safety(p0): never use trusted SCL semantic model for a different conf…
masarray Oct 17, 2026
e26a459
test(p0): guard against applying opened SCL schema to other MMS endpoint
masarray Oct 17, 2026
7158672
fix(p0): honor validated MMS DataRef aliases while enforcing exact ca…
masarray Oct 17, 2026
06d1b79
test(p0): verify two local q/t evidence inspectors and validated MMS …
masarray Oct 17, 2026
9b75c1b
safety(p0): validate exact MMS FCDA aliases and reject malformed 13-b…
masarray Oct 17, 2026
d15401c
test(p0): cover exact MMS DataRef alias, wrong FC/leaf and malformed …
masarray Oct 17, 2026
8259835
test(p0): guard malformed IEC Quality rejection alongside alias norma…
masarray Oct 17, 2026
7f7d28b
fix(p0): restore clean binder and apply exact alias, quality and Open…
masarray Oct 17, 2026
5d45f22
test(p0): prove native opened SCL model and reject unverified model s…
masarray Oct 17, 2026
c915b1c
test(p0): pin Open SCL semantic source handling in convergence guard
masarray Oct 17, 2026
6a76c08
fix(p0): compare native MMS DataSet ObjectName identity across SCL no…
masarray Oct 17, 2026
fc3ddf3
perf(p0): cap verified semantic detail to first 32 report members
masarray Oct 17, 2026
307e252
test(p0): prove native MMS DataSet-name alias in semantic binding
masarray Oct 17, 2026
b55452b
test(p0): guard DataSet normalization and bounded verified detail budget
masarray Oct 17, 2026
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
73 changes: 44 additions & 29 deletions apps/ied_simulator/qml/BrowserReportPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,10 @@ Rectangle {
Layout.fillWidth: true
}
Label {
text: "Positional · not SCL-labelled"
text: root.selectedReportMemberDetails().length > 0
&& root.selectedReportMemberDetails()[0].authority
? "SCL verified · MMS type checked"
: "Positional · not SCL-labelled"
font.pixelSize: 10
color: theme.muted
}
Expand All @@ -691,40 +694,52 @@ Rectangle {
Repeater {
model: root.selectedReportMemberIndex === receivedMember.index
? root.selectedReportMemberDetails() : []
delegate: Rectangle {
delegate: ColumnLayout {
id: semanticLeafRow
required property var modelData
required property int index
Layout.fillWidth: true
implicitHeight: 27
color: index % 2 ? theme.surfaceSoft : theme.surface
RowLayout {
anchors.fill: parent
anchors.leftMargin: 18
anchors.rightMargin: 9
spacing: 10
Label {
text: modelData.label
Layout.preferredWidth: 180
elide: Text.ElideRight
font.pixelSize: 11
color: theme.textSoft
}
Label {
text: modelData.value
Layout.fillWidth: true
elide: Text.ElideMiddle
font.pixelSize: 11
color: theme.text
ToolTip.visible: detailMouse.containsMouse && truncated
ToolTip.text: text
MouseArea {
id: detailMouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
spacing: 0
Rectangle {
Layout.fillWidth: true
implicitHeight: 29
color: semanticLeafRow.index % 2
? theme.surfaceSoft : theme.surface
RowLayout {
anchors.fill: parent
anchors.leftMargin: 18
anchors.rightMargin: 9
spacing: 10
Label {
text: semanticLeafRow.modelData.label
Layout.preferredWidth: 180
elide: Text.ElideRight
font.pixelSize: 11
color: theme.textSoft
}
Label {
text: semanticLeafRow.modelData.value
Layout.fillWidth: true
elide: Text.ElideMiddle
font.pixelSize: 11
color: theme.text
ToolTip.visible: detailMouse.containsMouse
ToolTip.text: semanticLeafRow.modelData.reference
|| semanticLeafRow.modelData.value
MouseArea {
id: detailMouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
}
}
}
BrowserEngineeringEvidence {
Comment thread
masarray marked this conversation as resolved.
theme: root.theme
engineeringEvidence: semanticLeafRow.modelData.engineeringEvidence
|| ({})
}
}
}
}
Expand Down
31 changes: 29 additions & 2 deletions apps/ied_simulator/src/BrowserReportMemberProjection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "MmsEngineeringEvidence.hpp"
#include "BrowserBoundedMmsPreview.hpp"
#include "BrowserReportSemanticBinding.hpp"
#include "ariec61850/mms/reporting.hpp"

#include <QString>
Expand Down Expand Up @@ -64,7 +65,9 @@ inline constexpr int kBrowserReportDetailChildren = 16;
const std::vector<mms::MmsReportValue>& members,
int& remainingTypedEvidence,
const int requestedVisibleMembers = kBrowserReportVisibleMemberLimit,
const bool includeWireDetails = false) {
const bool includeWireDetails = false,
const mms::MmsLiveModelDocument* trustedModel = nullptr,
const std::string_view dataSetReference = {}) {
BrowserReportMemberProjection projection;
const int visibleLimit = std::clamp(requestedVisibleMembers, 0, kBrowserReportVisibleMemberLimit);
const auto visibleCount = std::min(members.size(), static_cast<std::size_t>(visibleLimit));
Expand Down Expand Up @@ -113,7 +116,31 @@ inline constexpr int kBrowserReportDetailChildren = 16;
.arg(includeWireDetails && ordinal < kBrowserReportDetailMembers
? QStringLiteral(" — select for details")
: QString{});
if (includeWireDetails && ordinal < kBrowserReportDetailMembers) {
const auto verified = includeWireDetails && trustedModel &&
ordinal < kBrowserReportDetailMembers
? bindFlatSclReportStructure(*trustedModel, dataSetReference, member)
: std::nullopt;
if (verified) {
displayValue = QStringLiteral("SCL-verified structure · %1 field(s)")
.arg(static_cast<qulonglong>(verified->size()));
for (const auto& leaf : *verified) {
const auto preview = previewMmsValue(*leaf.value, kBrowserReportTextLimit);
const auto full = QString::fromStdString(leaf.reference);
const auto dot = full.lastIndexOf(QLatin1Char('.'));
const auto engineering = decodeEngineeringEvidence(full, *leaf.value);
const QString displayed = engineering.isEmpty()
? QString::fromStdString(preview.text)
: engineering.value(QStringLiteral("summary")).toString();
detailRows.push_back(QVariantMap{
{QStringLiteral("label"), dot >= 0 ? full.mid(dot + 1) : full},
{QStringLiteral("value"), displayed
+ (preview.truncated ? QStringLiteral(" … [truncated]") : QString{})},
{QStringLiteral("engineeringEvidence"), engineering},
{QStringLiteral("authority"), QStringLiteral("SCL exact / MMS type checked")},
{QStringLiteral("reference"), full}});
if (preview.truncated) projection.textTruncated = true;
}
} else if (includeWireDetails && ordinal < kBrowserReportDetailMembers) {
const auto count = std::min(children.size(),
static_cast<std::size_t>(kBrowserReportDetailChildren));
for (std::size_t child = 0; child < count; ++child) {
Expand Down
183 changes: 183 additions & 0 deletions apps/ied_simulator/src/BrowserReportSemanticBinding.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "ariec61850/mms/live_model.hpp"
#include "ariec61850/mms/reporting.hpp"
#include <algorithm>
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

// Native port of ARIEC61850 MmsSemanticReportValueProjector's fail-closed
// invariants. Read-only, no additional protocol stack or MMS association.
namespace arstack::iedsim {
namespace semantic_mms = ar::iec61850::mms;
struct VerifiedReportLeaf final {
std::string reference;
const semantic_mms::MmsDataValue* value{};
};
inline bool semanticInside(const std::string_view leaf,
const std::string_view root) {
return leaf.size() > root.size() && leaf.substr(0,root.size()) == root &&
leaf[root.size()] == '.';
}
inline semantic_mms::MmsDataKind semanticExpectedKind(const std::string_view basic) {
using K = semantic_mms::MmsDataKind;
if (basic == "BOOLEAN") return K::boolean;
if (basic == "Quality" || basic == "QUALITY") return K::bit_string;
if (basic == "Timestamp" || basic == "TIMESTAMP") return K::utc_time;
if (basic == "Enum" || basic == "ENUM" || basic == "INT32" ||
basic == "INT8" || basic == "INT16") return K::integer;
if (basic == "FLOAT32" || basic == "FLOAT64") return K::floating_point;
return K::unknown;
}
inline bool semanticKindCompatible(const semantic_mms::MmsDataValue& value,
const std::string_view basic) {
using K = semantic_mms::MmsDataKind;
if (basic == "BOOLEAN") return value.kind() == K::boolean;
if (basic == "Quality" || basic == "QUALITY") {
if (value.kind() != K::bit_string) return false;
const auto& bits = value.raw_value();
return bits.size() == 3U && bits[0] == 3U &&
(bits[2] & 0x07U) == 0U;
}
if (basic == "Timestamp" || basic == "TIMESTAMP") return value.kind() == K::utc_time;
if (basic == "Enum" || basic == "ENUM" || basic == "INT32" ||
basic == "INT8" || basic == "INT16") return value.kind() == K::integer;
if (basic == "FLOAT32" || basic == "FLOAT64")
return value.kind() == K::floating_point;
// Unknown/vendor type cannot authorize semantic labelling.
return false;
}
inline std::optional<std::vector<VerifiedReportLeaf>> bindFlatSclReportStructure(
const semantic_mms::MmsLiveModelDocument& model,
const std::string_view dataSet,
const semantic_mms::MmsReportValue& member) {
using namespace semantic_mms;
if (!member.value || member.value->kind() != MmsDataKind::structure)
return std::nullopt;
// SCL identity/model must be trusted and the FCDA directory must be
// uniquely bound to this DataSet and exact ordinal. Never infer by RptID.
// Canonical Open SCL and SCL-assisted contexts share the same existing
// MMS report runtime; the caller binds the selected model to this endpoint
// and the runtime has validated the frame against the live RCB/DataSet.
const bool openedScl = model.source == "OpenedSCL";
const bool trustedSnapshot = model.source == "TrustedSclInitialSnapshot";
if (model.identity.confidence != MmsLiveModelConfidence::exact ||
(!openedScl && !trustedSnapshot) || dataSet.empty())
return std::nullopt;
const MmsLiveDataSet* chosen = nullptr;
// Reuse the ObjectName normalization accepted by the native frame
// validator: dot and dollar forms must resolve to one DataSet identity.
try {
const auto requested =
MmsDataSetDirectoryCodec::parse_data_set_reference(std::string{dataSet});
for (const auto& ds : model.data_sets) {
if (MmsDataSetDirectoryCodec::parse_data_set_reference(ds.reference)
!= requested)
continue;
if (chosen) return std::nullopt;
chosen = &ds;
}
} catch (const std::exception&) {
return std::nullopt;
}
if (!chosen) return std::nullopt;
const MmsLiveDataSetMember* binding = nullptr;
for (const auto& fcda : chosen->members) {
if (fcda.index != member.data_set_index) continue;
if (binding) return std::nullopt;
binding = &fcda;
}
if (!binding || binding->confidence != MmsLiveModelConfidence::exact ||
binding->reference.empty() || binding->functional_constraint.empty())
return std::nullopt;
const auto& raw = member.data_reference;
const auto& dir = member.member;
// The native frame validator accepts IEC and MMS DataRef notation.
// Preserve those aliases only when they resolve to the exact same
// canonical object reference and functional constraint.
const auto matchesExactMember = [&](const std::string_view value) {
if (value.empty()) return false;
if (value == binding->reference) return true;
const auto slash = value.find('/');
if (slash == std::string_view::npos || slash == 0U) return false;
const auto fcStart = value.find('$', slash + 1U);
if (fcStart == std::string_view::npos) return false;
const auto pathStart = value.find('$', fcStart + 1U);
if (pathStart == std::string_view::npos ||
value.substr(fcStart + 1U, pathStart - fcStart - 1U) !=
binding->functional_constraint)
return false;
std::string canonical{value.substr(0, fcStart)};
canonical.push_back('.');
for (std::size_t i = pathStart + 1U; i < value.size(); ++i)
canonical.push_back(value[i] == '$' ? '.' : value[i]);
return canonical == binding->reference;
};
if (!raw.empty() && !matchesExactMember(raw)) return std::nullopt;
if (dir && !dir->user_reference.empty() &&
!matchesExactMember(dir->user_reference)) return std::nullopt;
if (dir && !dir->mms_reference.empty() &&
!matchesExactMember(dir->mms_reference)) return std::nullopt;
if (raw.empty() && (!dir || (dir->user_reference.empty() &&
dir->mms_reference.empty())))
return std::nullopt;
if (dir && !dir->functional_constraint.empty() &&
dir->functional_constraint != binding->functional_constraint)
return std::nullopt;
// Flat ST/CF/SG structures only. Nested, array, repeated or vendor
// layouts have no proven ordered schema here and remain positional.
const MmsLiveDataObject* object = nullptr;
for (const auto& ld : model.logical_devices)
for (const auto& ln : ld.logical_nodes)
for (const auto& candidate : ln.data_objects)
if (binding->reference == candidate.reference) {
if (object) return std::nullopt;
object = &candidate;
}
if (!object || object->confidence != MmsLiveModelConfidence::exact)
return std::nullopt;
std::vector<const MmsLiveDataAttribute*> attributes;
for (const auto& attribute : object->attributes) {
if (attribute.functional_constraint != binding->functional_constraint ||
!semanticInside(attribute.object_reference, binding->reference))
continue;
if (attribute.source !=
(openedScl ? "OpenedSCL" : "TrustedSCL") ||
attribute.type_confidence != MmsLiveModelConfidence::exact ||
attribute.functional_constraint_confidence != MmsLiveModelConfidence::exact)
return std::nullopt;
// Only one direct segment is admissible; nested structures demand
// matching a hierarchical SCL schema, not guesswork.
const auto suffix = std::string_view{attribute.object_reference}
.substr(binding->reference.size() + 1);
if (suffix.empty() || suffix.find('.') != std::string_view::npos)
return std::nullopt;
attributes.push_back(&attribute);
}
const auto& children = member.value->children();
if (attributes.empty() || attributes.size() != children.size() ||
children.size() > 16U) return std::nullopt;
// Positional order is not independently proven by matching count alone.
// Reject repeated scalar types, where a vendor permutation would look
// valid and produce deceptively named engineering values.
for (std::size_t i = 0; i < attributes.size(); ++i)
for (std::size_t j = i + 1; j < attributes.size(); ++j)
if (semanticExpectedKind(attributes[i]->scl_basic_type) ==
semanticExpectedKind(attributes[j]->scl_basic_type))
return std::nullopt;
Comment thread
masarray marked this conversation as resolved.
std::vector<VerifiedReportLeaf> result;
result.reserve(children.size());
for (std::size_t i = 0; i < children.size(); ++i) {
if (!semanticKindCompatible(children[i], attributes[i]->scl_basic_type))
return std::nullopt;
if (std::any_of(result.begin(), result.end(), [&](const auto& leaf){
return leaf.reference == attributes[i]->object_reference; }))
return std::nullopt;
result.push_back({attributes[i]->object_reference, &children[i]});
}
return result;
}
} // namespace arstack::iedsim
Loading
Loading