6eace4bbba
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.
64 lines
3.8 KiB
C#
64 lines
3.8 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
|
|
|
|
/// <summary>
|
|
/// The two shape rules an authored MQTT address must satisfy to be <em>publishable</em>, shared by
|
|
/// the deploy-time inspector (<see cref="MqttTagDefinitionFactory.Inspect"/>) and the write encoder
|
|
/// (<see cref="MqttWriteEncoder"/>) so the warning an operator is shown and the refusal the driver
|
|
/// makes can never disagree.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Neither rule gates the READ path, and that asymmetry is the point.</b> A tag whose
|
|
/// subscription <c>topic</c> carries a wildcard still reads (ambiguously — hence
|
|
/// <see cref="MqttTagDefinitionFactory.Inspect"/>'s long-standing advisory), and a Sparkplug tag
|
|
/// with a malformed id still parses. What these rules decide is whether an <em>outbound</em>
|
|
/// message could legally be addressed at all, so they belong to the write encoder and to the
|
|
/// inspector, never to the mappers' accept gate — which must stay exactly as strict as the
|
|
/// runtime reader (Gitea #551).
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Why a wildcard write destination is a refusal rather than a best effort.</b> MQTT-3.3.2-2
|
|
/// forbids a wildcard in a PUBLISH topic, and a conformant broker answers a protocol violation by
|
|
/// closing the network connection — which for this driver is the one connection every
|
|
/// subscription lives on. Publishing anyway would turn one mis-authored tag into a whole-driver
|
|
/// outage; refusing costs that tag its write and nothing else.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal static class MqttAddressRules
|
|
{
|
|
/// <summary>The MQTT wildcard characters — illegal in any concrete (published or subscribed) topic.</summary>
|
|
private static readonly char[] TopicWildcards = ['+', '#'];
|
|
|
|
/// <summary>
|
|
/// Characters that cannot appear in a <b>single MQTT topic segment</b>, which is what a Sparkplug
|
|
/// group / edge-node / device id is. <c>/</c> joins the wildcards here because it would silently
|
|
/// re-shape the command topic into a different address rather than making it unpublishable — and
|
|
/// because a decoded incoming id can never contain one, so an authored id that does is a binding
|
|
/// that could never have resolved.
|
|
/// </summary>
|
|
private static readonly char[] SegmentIllegalChars = ['/', '+', '#'];
|
|
|
|
/// <summary>
|
|
/// Whether <paramref name="topic"/> carries an MQTT wildcard (<c>+</c> / <c>#</c>).
|
|
/// </summary>
|
|
/// <param name="topic">A topic string; a <see langword="null"/>/empty one carries none.</param>
|
|
/// <returns><see langword="true"/> when a wildcard is present.</returns>
|
|
internal static bool HasWildcard(string? topic) =>
|
|
!string.IsNullOrEmpty(topic) && topic.IndexOfAny(TopicWildcards) >= 0;
|
|
|
|
/// <summary>
|
|
/// Whether <paramref name="id"/> is unusable as a single MQTT topic segment — i.e. as a Sparkplug
|
|
/// group, edge-node or device id.
|
|
/// </summary>
|
|
/// <param name="id">The authored id; a <see langword="null"/>/empty one is not judged here (absence
|
|
/// is handled by the mappers' own presence checks, and <c>deviceId</c> is legitimately absent).</param>
|
|
/// <returns><see langword="true"/> when the id contains <c>/</c>, <c>+</c> or <c>#</c>.</returns>
|
|
/// <remarks>
|
|
/// Deliberately NOT applied to <c>metricName</c>: a metric name rides in the Sparkplug
|
|
/// <em>payload</em>, not in the topic, and Sparkplug's own canonical names use <c>/</c> (e.g.
|
|
/// <c>Node Control/Rebirth</c>). Applying it there would refuse every conformant node-control tag.
|
|
/// </remarks>
|
|
internal static bool IsIllegalTopicSegment(string? id) =>
|
|
!string.IsNullOrEmpty(id) && id.IndexOfAny(SegmentIllegalChars) >= 0;
|
|
}
|