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
1 change: 1 addition & 0 deletions apps/ied_simulator/src/SclWorkspaceController_part_00.inc
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ bool sampledValuesEquivalent(
lhs.smv_id != rhs.smv_id || lhs.multicast != rhs.multicast ||
lhs.multicast_valid != rhs.multicast_valid || lhs.sample_rate != rhs.sample_rate ||
lhs.sample_mode != rhs.sample_mode || lhs.no_asdu != rhs.no_asdu ||
lhs.no_asdu_valid != rhs.no_asdu_valid ||
!(lhs.smv_options == rhs.smv_options)) {
return false;
}
Expand Down
5 changes: 5 additions & 0 deletions include/ariec61850/sampled_values/publisher_profile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ class SvPublisherProfileCompiler final {
}
profile.data_set_reference = stream.data_set_reference;
profile.configuration_revision = stream.configuration_revision;
if (!stream.no_asdu_valid) {
result.errors.push_back(
"SV SampledValueControl has invalid explicit nofASDU; "
"packetization cannot be inferred from malformed engineering input.");
}
profile.timing = resolve_sv_publication_timing(
parse_sample_mode(stream.sample_mode),
stream.sample_rate,
Expand Down
5 changes: 5 additions & 0 deletions include/ariec61850/scl/exporter_part_01.inc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ inline bool validate_model(
": SampledValueControl@multicast was explicitly malformed in the source.";
return false;
}
if (!stream.no_asdu_valid || stream.no_asdu == 0U) {
error = "Canonical SCL export rejected " + stream.control_block_reference +
": SampledValueControl@nofASDU was explicitly invalid in the source.";
return false;
}
}
for (const auto& entry : document.model_entries) {
if (entry.ied_name.empty() || entry.ld_inst.empty() || entry.ln_class.empty() ||
Expand Down
2 changes: 2 additions & 0 deletions include/ariec61850/scl/model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ struct SclSampledValuesStream final : SclProcessBusStream {
std::uint16_t sample_rate{};
std::string sample_mode;
std::uint16_t no_asdu{1U};
// Only omitted nofASDU may use the SCL default of 1.
bool no_asdu_valid{true};
SclSmvOptions smv_options;

friend bool operator==(const SclSampledValuesStream&, const SclSampledValuesStream&) = default;
Expand Down
24 changes: 21 additions & 3 deletions src/scl/parser_part_05_02.inc
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,27 @@

stream.sample_rate = ushort_attribute(*control, "smpRate");
stream.sample_mode = attribute(control, "smpMod");
stream.no_asdu = std::max<std::uint16_t>(
1U,
ushort_attribute(*control, "nofASDU"));
// Apply the default only for omission; explicit malformed
// nofASDU must never silently become a valid one-ASDU stream.
const bool no_asdu_present = std::any_of(
control->attributes.begin(), control->attributes.end(),
[](const XmlAttribute& item) {
return local_name(item.name) == "nofASDU";
});
if (no_asdu_present) {
const auto count = optional_uint_attribute(*control, "nofASDU");
if (!count.has_value() || *count == 0U ||
*count > std::numeric_limits<std::uint16_t>::max()) {
stream.no_asdu_valid = false;
stream.no_asdu = 0U;
document.warnings.push_back(
"SV " + stream.control_block_reference +
" has invalid explicit nofASDU='" + attribute(control, "nofASDU") +
"'; deployment/export is blocked.");
} else {
stream.no_asdu = static_cast<std::uint16_t>(*count);
}
}
if (const auto* options = first_child(*control, "SmvOpts"); options != nullptr) {
stream.smv_options.element_present = true;
stream.smv_options.refresh_time = bool_attribute(*options, "refreshTime");
Expand Down
55 changes: 55 additions & 0 deletions tests/test_scl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,60 @@ void parser_preserves_sampled_value_multicast_semantics() {
CHECK(invalid_export.error.find("multicast was explicitly malformed") != std::string::npos);
}

void parser_fails_closed_on_explicit_invalid_nof_asdu() {
using namespace ar::iec61850::sampled_values;
using namespace ar::iec61850::scl;

constexpr std::string_view xml = R"xml(
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" version="2007" revision="B">
<IED name="IED1"><AccessPoint><Server><LDevice inst="LD0"><LN0 lnClass="LLN0">
<SampledValueControl name="OMITTED" smpRate="4000" smpMod="SmpPerSec"/>
<SampledValueControl name="ONE" smpRate="4000" smpMod="SmpPerSec" nofASDU="1"/>
<SampledValueControl name="TWO" smpRate="4000" smpMod="SmpPerSec" nofASDU="2"/>
<SampledValueControl name="ZERO" smpRate="4000" smpMod="SmpPerSec" nofASDU="0"/>
<SampledValueControl name="TEXT" smpRate="4000" smpMod="SmpPerSec" nofASDU="not-a-number"/>
<SampledValueControl name="OVERFLOW" smpRate="4000" smpMod="SmpPerSec" nofASDU="65536"/>
<SampledValueControl name="EMPTY" smpRate="4000" smpMod="SmpPerSec" nofASDU=""/>
</LN0></LDevice></Server></AccessPoint></IED>
</SCL>)xml";
const auto document = SclParser{}.parse(xml, "invalid-nofasdu.scd");
CHECK(document.sampled_values_streams.size() == 7U);
for (std::size_t index = 0U; index < 3U; ++index) {
CHECK(document.sampled_values_streams[index].no_asdu_valid);
}
CHECK(document.sampled_values_streams[0].no_asdu == 1U);
CHECK(document.sampled_values_streams[1].no_asdu == 1U);
CHECK(document.sampled_values_streams[2].no_asdu == 2U);
for (std::size_t index = 3U; index < 7U; ++index) {
const auto& stream = document.sampled_values_streams[index];
CHECK(!stream.no_asdu_valid);
CHECK(stream.no_asdu == 0U);
SvPublisherProfileCompileContext context;
context.profile_family = SvProfileFamily::iec61850_9_2;
const auto compiled = SvPublisherProfileCompiler::compile(stream, context);
CHECK(!compiled.ok());
CHECK(std::any_of(compiled.errors.begin(), compiled.errors.end(),
[](const std::string& error) {
return error.find("invalid explicit nofASDU") != std::string::npos;
}));
}
CHECK(std::count_if(document.warnings.begin(), document.warnings.end(),
[](const std::string& warning) {
return warning.find("invalid explicit nofASDU=") != std::string::npos;
}) == 4);

auto modeled = SclParser{}.load(fixture("sv-4800-structured-4i4v.scd"));
CHECK(modeled.sampled_values_streams.size() == 1U);
SclCanonicalExportOptions options;
options.edition = modeled.edition;
options.profile = SclExportProfile::scd;
CHECK(SclExporter::canonical(modeled, options).success);
modeled.sampled_values_streams.front().no_asdu_valid = false;
const auto blocked = SclExporter::canonical(modeled, options);
CHECK(!blocked.success);
CHECK(blocked.error.find("nofASDU was explicitly invalid") != std::string::npos);
}

void dataset_reference_resolver_accepts_canonical_and_local_forms() {
using namespace ar::iec61850::scl;

Expand Down Expand Up @@ -1154,6 +1208,7 @@ int main() {
{"SV profile family and transport semantics", sampled_values_profile_family_and_transport_are_explicit},
{"IEC 61869-9 variant dataset and scaling semantics", iec61869_9_profile_resolves_variant_dataset_and_exact_scaling},
{"SCL SV multicast semantics", parser_preserves_sampled_value_multicast_semantics},
{"SCL SV nofASDU provenance", parser_fails_closed_on_explicit_invalid_nof_asdu},
{"SCL dataset references", dataset_reference_resolver_accepts_canonical_and_local_forms},
{"SCL configured control model", parser_preserves_configured_control_model_value},
{"Simulator semantic defaults and indexed RCBs", simulator_compiles_semantic_defaults_and_indexed_report_instances},
Expand Down
Loading