namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt; /// /// The two shape rules an authored MQTT address must satisfy to be publishable, shared by /// the deploy-time inspector () and the write encoder /// () so the warning an operator is shown and the refusal the driver /// makes can never disagree. /// /// /// /// Neither rule gates the READ path, and that asymmetry is the point. A tag whose /// subscription topic carries a wildcard still reads (ambiguously — hence /// 's long-standing advisory), and a Sparkplug tag /// with a malformed id still parses. What these rules decide is whether an outbound /// 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). /// /// /// Why a wildcard write destination is a refusal rather than a best effort. 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. /// /// internal static class MqttAddressRules { /// The MQTT wildcard characters — illegal in any concrete (published or subscribed) topic. private static readonly char[] TopicWildcards = ['+', '#']; /// /// Characters that cannot appear in a single MQTT topic segment, which is what a Sparkplug /// group / edge-node / device id is. / 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. /// private static readonly char[] SegmentIllegalChars = ['/', '+', '#']; /// /// Whether carries an MQTT wildcard (+ / #). /// /// A topic string; a /empty one carries none. /// when a wildcard is present. internal static bool HasWildcard(string? topic) => !string.IsNullOrEmpty(topic) && topic.IndexOfAny(TopicWildcards) >= 0; /// /// Whether is unusable as a single MQTT topic segment — i.e. as a Sparkplug /// group, edge-node or device id. /// /// The authored id; a /empty one is not judged here (absence /// is handled by the mappers' own presence checks, and deviceId is legitimately absent). /// when the id contains /, + or #. /// /// Deliberately NOT applied to metricName: a metric name rides in the Sparkplug /// payload, not in the topic, and Sparkplug's own canonical names use / (e.g. /// Node Control/Rebirth). Applying it there would refuse every conformant node-control tag. /// internal static bool IsIllegalTopicSegment(string? id) => !string.IsNullOrEmpty(id) && id.IndexOfAny(SegmentIllegalChars) >= 0; }