diff --git a/apps/ied_simulator/src/BrowserBoundedMmsPreview.hpp b/apps/ied_simulator/src/BrowserBoundedMmsPreview.hpp new file mode 100644 index 000000000..d27da3427 --- /dev/null +++ b/apps/ied_simulator/src/BrowserBoundedMmsPreview.hpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/data_codec.hpp" +#include "ariec61850/mms/data_value.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +// Presentation-only, bounded MMS formatting. The canonical codec and the +// original decoded value are unchanged. Scalar formatting delegates to the +// canonical codec; only potentially unbounded strings, raw bytes and nested +// collections are streamed with a character budget. +namespace arstack::iedsim { + +struct BoundedMmsPreview final { + std::string text; + bool truncated{}; +}; + +namespace preview_detail { +struct Writer final { + BoundedMmsPreview result; + std::size_t limit{}; + + explicit Writer(const std::size_t maximum) : limit(maximum) { + result.text.reserve(std::min(maximum, std::size_t{1024})); + } + + void append(const std::string_view text) { + if (text.empty()) return; + const auto available = limit - result.text.size(); + const auto count = std::min(available, text.size()); + result.text.append(text.data(), count); + if (count < text.size()) result.truncated = true; + } + + // Avoid cutting a valid UTF-8 sequence in half on a byte budget. + // IEC VisibleString is usually ASCII; MMSString may contain UTF-8. + void appendUtf8(const std::string_view text) { + if (text.empty()) return; + const auto available = limit - result.text.size(); + auto count = std::min(available, text.size()); + if (count < text.size()) { + while (count > 0 && + (static_cast(text[count]) & 0xC0U) == 0x80U) + --count; + } + result.text.append(text.data(), count); + if (count < text.size()) result.truncated = true; + } + + void appendChar(const char ch) { + if (result.text.size() < limit) result.text.push_back(ch); + else result.truncated = true; + } + + void appendHex(const std::vector& bytes) { + static constexpr char hex[] = "0123456789ABCDEF"; + for (const auto byte : bytes) { + if (result.text.size() >= limit) { + result.truncated = true; + return; + } + appendChar(hex[(byte >> 4U) & 0x0FU]); + appendChar(hex[byte & 0x0FU]); + if (result.truncated) return; + } + } +}; + +inline void appendMmsValue(Writer& writer, + const ar::iec61850::mms::MmsDataValue& value, + const unsigned depth) { + using namespace ar::iec61850::mms; + if (writer.result.truncated) return; + switch (value.kind()) { + case MmsDataKind::visible_string: + case MmsDataKind::mms_string: + writer.appendUtf8(std::get(value.value())); + return; + case MmsDataKind::array: + case MmsDataKind::structure: { + if (depth >= 16U) { + writer.append("..."); + writer.result.truncated = true; + return; + } + writer.appendChar(value.kind() == MmsDataKind::array ? '[' : '{'); + const auto& children = value.children(); + for (std::size_t index = 0; index < children.size(); ++index) { + if (index != 0U) writer.append(", "); + if (writer.result.truncated) break; + appendMmsValue(writer, children[index], depth + 1U); + if (writer.result.truncated) break; + } + if (!writer.result.truncated) + writer.appendChar(value.kind() == MmsDataKind::array ? ']' : '}'); + return; + } + case MmsDataKind::bit_string: + case MmsDataKind::octet_string: + case MmsDataKind::binary_time: + case MmsDataKind::unknown: + case MmsDataKind::bcd: + case MmsDataKind::boolean_array: + case MmsDataKind::object_id: + writer.appendHex(value.raw_value()); + return; + default: + // All remaining scalar display strings have bounded size; formatting + // belongs to the canonical codec, not a parallel IEC decoder. + writer.append(MmsDataCodec::to_display_string(value)); + return; + } +} +} // namespace preview_detail + +[[nodiscard]] inline BoundedMmsPreview previewMmsValue( + const ar::iec61850::mms::MmsDataValue& value, + const std::size_t maximumCharacters) { + preview_detail::Writer writer(maximumCharacters); + preview_detail::appendMmsValue(writer, value, 0U); + return std::move(writer.result); +} + +} // namespace arstack::iedsim diff --git a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp index 14554f6d2..9364d505d 100644 --- a/apps/ied_simulator/src/BrowserReportMemberProjection.hpp +++ b/apps/ied_simulator/src/BrowserReportMemberProjection.hpp @@ -2,7 +2,7 @@ #pragma once #include "MmsEngineeringEvidence.hpp" -#include "ariec61850/mms/data_codec.hpp" +#include "BrowserBoundedMmsPreview.hpp" #include "ariec61850/mms/reporting.hpp" #include @@ -55,8 +55,14 @@ inline constexpr int kBrowserReportTextLimit = 1024; displayReference = QStringLiteral("member[%1]").arg( static_cast(member.data_set_index)); QString displayValue = QStringLiteral(""); - if (member.value) - displayValue = QString::fromStdString(mms::MmsDataCodec::to_display_string(*member.value)); + 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; + } QString reasons; for (const auto& reason : member.reason_for_inclusion.names) { if (!reasons.isEmpty()) reasons += QStringLiteral(", "); diff --git a/apps/ied_simulator/src/ied_browser_large_model_qa.cpp b/apps/ied_simulator/src/ied_browser_large_model_qa.cpp index 39e2c419b..1f3d37671 100644 --- a/apps/ied_simulator/src/ied_browser_large_model_qa.cpp +++ b/apps/ied_simulator/src/ied_browser_large_model_qa.cpp @@ -2,6 +2,7 @@ #include "MmsLiveTreeModel.hpp" #include "MmsEngineeringEvidence.hpp" #include "BrowserReportMemberProjection.hpp" +#include "BrowserBoundedMmsPreview.hpp" #include #include @@ -354,6 +355,73 @@ int main(int argc, char** argv) { return 28; } + using ar::iec61850::mms::MmsDataCodec; + using ar::iec61850::mms::MmsDataValue; + // M2F: the GUI must not allocate multi-megabyte intermediary strings + // for a tiny report preview. Primitive display remains codec-identical. + const std::string hugeText(2'000'000, 'V'); + const auto hugeVisible = MmsDataValue::visible_string(hugeText); + const auto hugePreview = arstack::iedsim::previewMmsValue(hugeVisible, 1024); + const std::vector hugeRawBytes(1'000'000, 0xABU); + const auto hugeBytes = MmsDataValue::octet_string(hugeRawBytes); + const auto hexPreview = arstack::iedsim::previewMmsValue(hugeBytes, 1024); + std::vector children; + children.reserve(4000); + for (int index = 0; index < 4000; ++index) + children.push_back(MmsDataValue::visible_string(std::string(300, 'C'))); + const auto nested = MmsDataValue::array(std::move(children)); + const auto nestedPreview = arstack::iedsim::previewMmsValue(nested, 1024); + if (hugePreview.text.size() != 1024 || !hugePreview.truncated || + hugePreview.text.front() != 'V' || hugePreview.text.back() != 'V' || + hexPreview.text.size() != 1024 || !hexPreview.truncated || + hexPreview.text.substr(0, 4) != "ABAB" || + nestedPreview.text.size() != 1024 || !nestedPreview.truncated || + nestedPreview.text.front() != '[') { + std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL bounded_value_serialization\n"; + return 29; + } + // Exact semantics for complete output, including nested structures, + // raw-byte uppercase hex, bool and UTC types. + const std::vector samples{ + MmsDataValue::boolean(true), + MmsDataValue::integer(-42), + MmsDataValue::floating_point(3.125), + MmsDataValue::bit_string(3U, bits), + MmsDataValue::utc_time(utc), + MmsDataValue::structure({MmsDataValue::boolean(false), + MmsDataValue::visible_string("ABC")})}; + for (const auto& sample : samples) { + const auto preview = arstack::iedsim::previewMmsValue(sample, 1024); + if (preview.truncated || + preview.text != MmsDataCodec::to_display_string(sample)) { + std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL bounded_value_parity\n"; + return 30; + } + } + const auto zeroBudget = arstack::iedsim::previewMmsValue(hugeVisible, 0); + if (!zeroBudget.text.empty() || !zeroBudget.truncated) { + std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL bounded_value_zero_budget\n"; + return 31; + } + // A bounded MMSString must not end on an incomplete UTF-8 character. + const std::string utf8Boundary = std::string(1023, 'A') + "\xE2\x82\xAC"; + const auto utf8Preview = arstack::iedsim::previewMmsValue( + MmsDataValue::mms_string(utf8Boundary), 1024); + if (!utf8Preview.truncated || utf8Preview.text.size() != 1023 || + utf8Preview.text.back() != 'A') { + std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL bounded_value_utf8_boundary\n"; + return 33; + } + largeFrame[5].value = hugeVisible; + int hugeBudget = 4; + const auto boundedFrame = arstack::iedsim::projectBrowserReportMembers( + largeFrame, hugeBudget, 6); + if (boundedFrame.values.size() != 6 || !boundedFrame.textTruncated || + boundedFrame.values.at(5).size() > 1100) { + std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL bounded_value_report_integration\n"; + return 32; + } + const bool passes = applyMs <= 5000 && filterMs <= 1500 && selectMs <= 500; std::cout << "IED_BROWSER_LARGE_MODEL_" << (passes ? "PASS" : "FAIL") << " nodes=" << model.totalNodeCount() diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index da8d7063d..4f0499ff0 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -110,6 +110,8 @@ 'projection.rowsTruncated = members.size() > visibleCount', 'if (!member.value || reference.isEmpty()) continue', 'decodeEngineeringEvidence(', + '#include "BrowserBoundedMmsPreview.hpp"', + 'previewMmsValue(*member.value', ]: if token not in projection_header: raise SystemExit("BROWSER_REPORT_BOUNDED_PROJECTION_FAIL missing=" + token) @@ -117,6 +119,20 @@ raise SystemExit("BROWSER_REPORT_BOUNDED_PROJECTION_FAIL full-frame QML string conversion") if 'decodeEngineeringEvidence(' in report_controller: 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") + +value_preview = (workspace_path.parent.parent / "src" / "BrowserBoundedMmsPreview.hpp").read_text(encoding="utf-8") +for token in [ + 'BoundedMmsPreview', + 'previewMmsValue(', + 'writer.appendHex(value.raw_value())', + 'writer.appendUtf8(std::get(value.value()))', + 'writer.result.truncated = true', + 'value.children()', +]: + if token not in value_preview: + raise SystemExit("BROWSER_VALUE_BOUNDED_FAIL missing=" + token) report_required = [ 'objectName: "browserReportUnifiedInspector"',