// Copyright 2023-2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace ZB.MOM.NatsNet.Server.Internal.DataStructures; /// /// Utility methods for NATS subject matching, wildcard part decomposition, /// common prefix computation, and byte manipulation used by SubjectTree. /// internal static class SubjectTreeParts { // NATS subject special bytes. internal const byte Pwc = (byte)'*'; // single-token wildcard internal const byte Fwc = (byte)'>'; // full wildcard (terminal) internal const byte TSep = (byte)'.'; // token separator // Sentinel pivot returned when subject position is past end. internal const byte NoPivot = 127; /// /// Returns the pivot byte at in , /// or if the position is at or beyond the end. /// internal static byte Pivot(ReadOnlySpan subject, int pos) => pos >= subject.Length ? NoPivot : subject[pos]; /// /// Returns the pivot byte at in , /// or if the position is at or beyond the end. /// internal static byte Pivot(byte[] subject, int pos) => pos >= subject.Length ? NoPivot : subject[pos]; /// /// Computes the number of leading bytes that are equal between two spans. /// internal static int CommonPrefixLen(ReadOnlySpan s1, ReadOnlySpan s2) { var limit = Math.Min(s1.Length, s2.Length); var i = 0; while (i < limit && s1[i] == s2[i]) i++; return i; } /// /// Returns a copy of , or an empty array if src is empty. /// internal static byte[] CopyBytes(ReadOnlySpan src) { if (src.IsEmpty) return Array.Empty(); return src.ToArray(); } /// /// Returns a copy of , or an empty array if src is null or empty. /// internal static byte[] CopyBytes(byte[]? src) { if (src == null || src.Length == 0) return Array.Empty(); var dst = new byte[src.Length]; src.CopyTo(dst, 0); return dst; } /// /// Converts a byte array to a string using Latin-1 (ISO-8859-1) encoding, /// which preserves a 1:1 byte-to-char mapping for all byte values 0-255. /// internal static string BytesToString(byte[] bytes) { if (bytes.Length == 0) return string.Empty; return System.Text.Encoding.Latin1.GetString(bytes); } /// /// Breaks a filter subject into parts separated by wildcards ('*' and '>'). /// Each literal segment between wildcards becomes one part; each wildcard /// becomes its own single-byte part. /// internal static byte[][] GenParts(byte[] filter) { var parts = new List(8); var start = 0; var e = filter.Length - 1; for (var i = 0; i < filter.Length; i++) { if (filter[i] == TSep) { // Check if next token is pwc (internal or terminal). if (i < e && filter[i + 1] == Pwc && ((i + 2 <= e && filter[i + 2] == TSep) || i + 1 == e)) { if (i > start) parts.Add(filter[start..(i + 1)]); parts.Add(filter[(i + 1)..(i + 2)]); i++; // skip pwc if (i + 2 <= e) i++; // skip next tsep from next part start = i + 1; } else if (i < e && filter[i + 1] == Fwc && i + 1 == e) { if (i > start) parts.Add(filter[start..(i + 1)]); parts.Add(filter[(i + 1)..(i + 2)]); i++; // skip fwc start = i + 1; } } else if (filter[i] == Pwc || filter[i] == Fwc) { // Wildcard must be preceded by tsep (or be at start). var prev = i - 1; if (prev >= 0 && filter[prev] != TSep) continue; // Wildcard must be at end or followed by tsep. var next = i + 1; if (next == e || (next < e && filter[next] != TSep)) continue; // Full wildcard must be terminal. if (filter[i] == Fwc && i < e) break; // Leading wildcard. parts.Add(filter[i..(i + 1)]); if (i + 1 <= e) i++; // skip next tsep start = i + 1; } } if (start < filter.Length) { // Eat leading tsep if present. if (filter[start] == TSep) start++; if (start < filter.Length) parts.Add(filter[start..]); } return parts.ToArray(); } /// /// Matches parts against a fragment (prefix or suffix). /// Returns the remaining parts and whether matching succeeded. /// internal static (byte[][] remainingParts, bool matched) MatchParts(byte[][] parts, byte[] frag) { var lf = frag.Length; if (lf == 0) return (parts, true); var si = 0; var lpi = parts.Length - 1; for (var i = 0; i < parts.Length; i++) { if (si >= lf) return (parts[i..], true); var part = parts[i]; var lp = part.Length; // Check for wildcard placeholders. if (lp == 1) { if (part[0] == Pwc) { // Find the next token separator. var index = Array.IndexOf(frag, TSep, si); if (index < 0) { // No tsep found. if (i == lpi) return (Array.Empty(), true); return (parts[i..], true); } si = index + 1; continue; } else if (part[0] == Fwc) { return (Array.Empty(), true); } } var end = Math.Min(si + lp, lf); // If part is larger than the remaining fragment, adjust. var comparePart = part; if (si + lp > end) comparePart = part[..(end - si)]; if (!frag.AsSpan(si, end - si).SequenceEqual(comparePart)) return (parts, false); // Fragment still has bytes left. if (end < lf) { si = end; continue; } // We matched a partial part. if (end < si + lp) { if (end >= lf) { // Create a copy of parts with the current part trimmed. var newParts = new byte[parts.Length][]; parts.CopyTo(newParts, 0); newParts[i] = parts[i][(lf - si)..]; return (newParts[i..], true); } else { return (parts[(i + 1)..], true); } } if (i == lpi) return (Array.Empty(), true); si += part.Length; } return (parts, false); } }