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
78 changes: 61 additions & 17 deletions apps/ied_simulator/src/BrowserReportMemberProjection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
#include <utility>

Expand All @@ -32,6 +34,29 @@ struct BrowserReportMemberProjection final {
inline constexpr int kBrowserReportVisibleMemberLimit = 256;
inline constexpr int kBrowserReportTextLimit = 1024;

// Bound untrusted IEC member reference and reason strings BEFORE conversion to
// QString. No full-sized temporary allocation for oversized MMS metadata.
[[nodiscard]] inline QString boundedReportMetadata(
const std::string_view source, const std::size_t maximumBytes, bool& truncated) {
std::size_t count = std::min(maximumBytes, source.size());
if (count < source.size()) {
// Never split a UTF-8 continuation byte at the preview boundary.
while (count > 0 && (static_cast<unsigned char>(source[count]) & 0xC0U) == 0x80U)
--count;
truncated = true;
}
return QString::fromUtf8(source.data(), static_cast<qsizetype>(count));
}

[[nodiscard]] inline bool hasReportTypedLeaf(
const std::string_view reference, const mms::MmsDataKind kind) noexcept {
if (reference.size() < 2U) return false;
const char separator = reference[reference.size() - 2U];
if (separator != '$' && separator != '.') return false;
return (reference.back() == 'q' && kind == mms::MmsDataKind::bit_string) ||
(reference.back() == 't' && kind == mms::MmsDataKind::utc_time);
}

[[nodiscard]] inline BrowserReportMemberProjection projectBrowserReportMembers(
const std::vector<mms::MmsReportValue>& members,
int& remainingTypedEvidence,
Expand All @@ -44,42 +69,67 @@ inline constexpr int kBrowserReportTextLimit = 1024;

for (std::size_t ordinal = 0; ordinal < visibleCount; ++ordinal) {
const auto& member = members[ordinal];
QString reference = QString::fromStdString(member.data_reference);
if (reference.isEmpty() && member.member)
reference = QString::fromStdString(member.member->user_reference);
if (reference.isEmpty() && member.member)
reference = QString::fromStdString(member.member->mms_reference);
const std::string* rawReference = &member.data_reference;
if (rawReference->empty() && member.member)
rawReference = &member.member->user_reference;
if (rawReference->empty() && member.member)
rawReference = &member.member->mms_reference;
const std::string_view fullReference(*rawReference);
bool referenceTruncated = false;
const QString reference = boundedReportMetadata(
fullReference, kBrowserReportTextLimit, referenceTruncated);

QString displayReference = reference;
if (displayReference.isEmpty())
displayReference = QStringLiteral("member[%1]").arg(
static_cast<qulonglong>(member.data_set_index));
QString displayValue = QStringLiteral("<failed>");
bool valueTruncated = false;
if (member.value) {
// Never allocate a full string/hex/array representation and then
// truncate it. Serialize directly into a 1024-character budget.
const auto preview = previewMmsValue(*member.value,
kBrowserReportTextLimit);
displayValue = QString::fromStdString(preview.text);
projection.textTruncated |= preview.truncated;
valueTruncated = preview.truncated;
}
QString reasons;
bool reasonsTruncated = false;
for (const auto& reason : member.reason_for_inclusion.names) {
if (reasons.size() >= kBrowserReportTextLimit) {
reasonsTruncated = true;
break;
}
if (!reasons.isEmpty()) reasons += QStringLiteral(", ");
reasons += QString::fromStdString(reason);
reasons += boundedReportMetadata(
reason, static_cast<std::size_t>(
std::max(0, kBrowserReportTextLimit - int(reasons.size()))),
reasonsTruncated);
if (reasonsTruncated) break;
}
QString text = QStringLiteral("%1 = %2%3").arg(
displayReference, displayValue,
reasons.isEmpty() ? QString{} : QStringLiteral(" · ") + reasons);
if (text.size() > kBrowserReportTextLimit) {
const bool rowTruncated = referenceTruncated || reasonsTruncated ||
(member.value && valueTruncated) ||
text.size() > kBrowserReportTextLimit;
if (rowTruncated) {
projection.textTruncated = true;
text = text.left(kBrowserReportTextLimit) + QStringLiteral(" … [preview truncated]");
const QString marker = QStringLiteral(" … [preview truncated]");
text = text.left(kBrowserReportTextLimit - marker.size()) + marker;
}
projection.values.push_back(std::move(text));

// Only read successful typed q/t values, and only when the retained
// presentation row matches the member ordinal (never DataSet position).
if (!member.value || reference.isEmpty()) continue;
if (!member.value || fullReference.empty()) continue;
if (referenceTruncated) {
// A shortened IEC reference must never be treated as the identity
// of a report q/t DA. The runtime retains the complete reference.
if (hasReportTypedLeaf(fullReference, member.value->kind()))
projection.evidenceTruncated = true;
continue;
}
if (remainingTypedEvidence > 0) {
const auto decoded = decodeEngineeringEvidence(reference, *member.value);
if (!decoded.isEmpty()) {
Expand All @@ -90,13 +140,7 @@ inline constexpr int kBrowserReportTextLimit = 1024;
}
} else {
// Do not decode (allocate rows) once the shared budget is empty.
const auto separator = std::max(
reference.lastIndexOf(QLatin1Char('$')),
reference.lastIndexOf(QLatin1Char('.')));
const QString leaf = reference.mid(separator + 1);
const auto kind = member.value->kind();
if ((leaf == QStringLiteral("q") && kind == mms::MmsDataKind::bit_string) ||
(leaf == QStringLiteral("t") && kind == mms::MmsDataKind::utc_time))
if (hasReportTypedLeaf(fullReference, member.value->kind()))
projection.evidenceTruncated = true;
}
}
Expand Down
45 changes: 45 additions & 0 deletions apps/ied_simulator/src/ied_browser_large_model_qa.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,51 @@ int main(int argc, char** argv) {
return 32;
}

using ar::iec61850::mms::MmsReportValue;
// M2G: bounded value formatting alone is insufficient if an IED returns
// oversized IEC member references or Reason-for-Inclusion names.
std::vector<MmsReportValue> giantMetadata(4);
giantMetadata[0].data_reference = std::string(1'500'000, 'R');
giantMetadata[0].value = MmsDataValue::boolean(true);
giantMetadata[1].data_reference = "LD4/LLN19.DO99.stVal";
giantMetadata[1].value = MmsDataValue::boolean(false);
giantMetadata[1].reason_for_inclusion.names.push_back(std::string(1'500'000, 'N'));
giantMetadata[2].data_reference = std::string(1'500'000, 'R') + ".q";
giantMetadata[2].value = MmsDataValue::bit_string(3U, bits);
giantMetadata[3].data_reference = "LD4/LLN19.DO99.stVal";
giantMetadata[3].value = MmsDataValue::boolean(true);
giantMetadata[3].reason_for_inclusion.names.assign(1500, "data-change");
int metadataBudget = 8;
const auto safeMetadata = arstack::iedsim::projectBrowserReportMembers(
giantMetadata, metadataBudget);
if (safeMetadata.values.size() != 4 || !safeMetadata.textTruncated ||
!safeMetadata.evidenceTruncated || !safeMetadata.evidence.isEmpty() ||
metadataBudget != 8 ||
safeMetadata.values.at(0).size() > 1024 ||
safeMetadata.values.at(1).size() > 1024 ||
safeMetadata.values.at(2).size() > 1024 ||
safeMetadata.values.at(3).size() > 1024 ||
!safeMetadata.values.at(0).contains(QStringLiteral("preview truncated")) ||
!safeMetadata.values.at(1).contains(QStringLiteral("preview truncated")) ||
!safeMetadata.values.at(3).contains(QStringLiteral("preview truncated"))) {
std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL bounded_report_metadata\n";
return 34;
}
// Normal IEC references remain exact so q/t evidence can still be
// associated with the original received member ordinal.
std::vector<MmsReportValue> normalMetadata(1);
normalMetadata[0].data_reference = "LD4/LLN19.DO99.q";
normalMetadata[0].value = MmsDataValue::bit_string(3U, bits);
const auto normalProjected = arstack::iedsim::projectBrowserReportMembers(
normalMetadata, metadataBudget);
if (normalProjected.evidence.size() != 1 ||
normalProjected.evidence.constFirst().toMap()
.value(QStringLiteral("index")).toInt() != 0 ||
normalProjected.textTruncated) {
std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL report_metadata_provenance\n";
return 35;
}

const bool passes = applyMs <= 5000 && filterMs <= 1500 && selectMs <= 500;
std::cout << "IED_BROWSER_LARGE_MODEL_" << (passes ? "PASS" : "FAIL")
<< " nodes=" << model.totalNodeCount()
Expand Down
10 changes: 9 additions & 1 deletion apps/ied_simulator/test_browser_convergence.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,13 @@
'kBrowserReportTextLimit = 1024',
'std::min(members.size(),',
'projection.rowsTruncated = members.size() > visibleCount',
'if (!member.value || reference.isEmpty()) continue',
'if (!member.value || fullReference.empty()) continue',
'decodeEngineeringEvidence(',
'boundedReportMetadata(',
'const std::string_view fullReference(*rawReference)',
'if (referenceTruncated) {',
'hasReportTypedLeaf(',
'text.left(kBrowserReportTextLimit - marker.size()) + marker',
'#include "BrowserBoundedMmsPreview.hpp"',
'previewMmsValue(*member.value',
]:
Expand All @@ -121,6 +126,9 @@
raise SystemExit("BROWSER_REPORT_BOUNDED_PROJECTION_FAIL duplicate inline decoding")
if 'MmsDataCodec::to_display_string(*member.value)' in projection_header:
raise SystemExit("BROWSER_VALUE_BOUNDED_FAIL unbounded intermediary in report GUI")
if ('QString::fromStdString(member.data_reference)' in projection_header or
'QString::fromStdString(reason)' in projection_header):
raise SystemExit("BROWSER_REPORT_METADATA_BOUNDED_FAIL eager untrusted metadata conversion")

value_preview = (workspace_path.parent.parent / "src" / "BrowserBoundedMmsPreview.hpp").read_text(encoding="utf-8")
for token in [
Expand Down
Loading