fix(mqtt-contracts): refuse unpublishable write destinations and advise on them at deploy
A wildcard writeTopic was published verbatim. MQTT-3.3.2-2 forbids a wildcard in a PUBLISH topic and a conformant broker answers a protocol violation by closing the connection — which for this driver is the one connection every subscription lives on, so a single write to a mis-authored tag took the whole driver dark. The Sparkplug side had the same hole plus a worse one: the binding tuple IS the NCMD/DCMD topic, so a '/' in groupId/edgeNodeId/deviceId silently addressed a different edge node than the tag names. MqttWriteEncoder now refuses both as NoWriteDestination (-> BadNotWritable); the read path is untouched. metricName is exempt on purpose — it rides in the payload and Sparkplug's own canonical names use '/'. MqttTagDefinitionFactory.Inspect gained the matching ADVISORIES (never rejections — the mappers accept these blobs, and the gate must never be stricter than the runtime). Both rules were already hard errors in the AdminUI's typed editor, but that is one authoring path of several: the raw-JSON textarea, CSV import and the API all reach the deploy gate directly, so the authoring surface was stricter than the gate — the drift class the #549-#559 arc exists to close. Shared shape rules single-sourced in the new internal MqttAddressRules. Driver.Mqtt.Tests 724 (was 708); AdminUI.Tests 1466; ControlPlane.Tests 261.
This commit is contained in:
@@ -19,9 +19,11 @@ public enum MqttWriteEncodeError
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The tag declares no write destination: a Plain tag with no <c>writeTopic</c>. Deliberately a
|
||||
/// hard refusal — see <see cref="MqttWriteEncoder"/>'s remarks on why a silent no-op would be
|
||||
/// worse than the honest read-only behaviour this replaced.
|
||||
/// The tag declares no <b>usable</b> write destination: a Plain tag with no <c>writeTopic</c> or
|
||||
/// one carrying an MQTT wildcard, or a Sparkplug tag whose binding tuple is incomplete or whose
|
||||
/// group/edge-node/device id is not a legal topic segment (see <see cref="MqttAddressRules"/>).
|
||||
/// Deliberately a hard refusal — see <see cref="MqttWriteEncoder"/>'s remarks on why a silent
|
||||
/// no-op would be worse than the honest read-only behaviour this replaced.
|
||||
/// </summary>
|
||||
NoWriteDestination,
|
||||
|
||||
@@ -121,6 +123,17 @@ public static class MqttWriteEncoder
|
||||
return MqttWriteEncodeError.NoWriteDestination;
|
||||
}
|
||||
|
||||
// A wildcard topic is not a destination — MQTT-3.3.2-2 forbids one in a PUBLISH, and a
|
||||
// conformant broker answers a protocol violation by CLOSING the connection, which is the same
|
||||
// connection every one of this driver's subscriptions lives on. So publishing anyway would turn
|
||||
// one mis-authored tag into a whole-driver outage. `NoWriteDestination` (→ BadNotWritable) is the
|
||||
// right verdict rather than a new error: the tag genuinely has nowhere a write could be sent, and
|
||||
// the read path is untouched. See MqttAddressRules.
|
||||
if (MqttAddressRules.HasWildcard(def.WriteTopic))
|
||||
{
|
||||
return MqttWriteEncodeError.NoWriteDestination;
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
// No MQTT payload means "the value is null" — a zero-length payload is a legitimate
|
||||
@@ -175,6 +188,18 @@ public static class MqttWriteEncoder
|
||||
return MqttWriteEncodeError.NoWriteDestination;
|
||||
}
|
||||
|
||||
// …and an id that is not a legal topic SEGMENT is the same failure by a different route: the
|
||||
// tuple is assembled into the NCMD/DCMD topic, so a '/' would silently address a different node
|
||||
// than the tag names while '+'/'#' would make the PUBLISH a protocol violation (see the Plain
|
||||
// branch). `metricName` is exempt on purpose — it rides in the payload, and Sparkplug's own
|
||||
// canonical names use '/'.
|
||||
if (MqttAddressRules.IsIllegalTopicSegment(groupId)
|
||||
|| MqttAddressRules.IsIllegalTopicSegment(edgeNodeId)
|
||||
|| MqttAddressRules.IsIllegalTopicSegment(def.DeviceId))
|
||||
{
|
||||
return MqttWriteEncodeError.NoWriteDestination;
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
return MqttWriteEncodeError.ValueNotRepresentable;
|
||||
|
||||
Reference in New Issue
Block a user