Session 07 scope (5 features, 17 tests, ~1165 Go LOC): - Protocol/ParserTypes.cs: ParserState enum (79 states), PublishArgument, ParseContext - Protocol/IProtocolHandler.cs: handler interface decoupling parser from client - Protocol/ProtocolParser.cs: Parse(), ProtoSnippet(), OverMaxControlLineLimit(), ProcessPub/HeaderPub/RoutedMsgArgs/RoutedHeaderMsgArgs, ClonePubArg(), GetHeader() - tests/Protocol/ProtocolParserTests.cs: 17 tests via TestProtocolHandler stub Auth extras from session 06 (committed separately): - Auth/TpmKeyProvider.cs, Auth/CertificateIdentityProvider/, Auth/CertificateStore/ Internal utilities & data structures (session 06 overflow): - Internal/AccessTimeService.cs, ElasticPointer.cs, SystemMemory.cs, ProcessStatsProvider.cs - Internal/DataStructures/GenericSublist.cs, HashWheel.cs - Internal/DataStructures/SubjectTree.cs, SubjectTreeNode.cs, SubjectTreeParts.cs All 461 tests pass (460 unit + 1 integration). DB updated for features 2588-2592 and tests 2598-2614.
243 lines
8.1 KiB
C#
243 lines
8.1 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// Utility methods for NATS subject matching, wildcard part decomposition,
|
|
/// common prefix computation, and byte manipulation used by SubjectTree.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Returns the pivot byte at <paramref name="pos"/> in <paramref name="subject"/>,
|
|
/// or <see cref="NoPivot"/> if the position is at or beyond the end.
|
|
/// </summary>
|
|
internal static byte Pivot(ReadOnlySpan<byte> subject, int pos)
|
|
=> pos >= subject.Length ? NoPivot : subject[pos];
|
|
|
|
/// <summary>
|
|
/// Returns the pivot byte at <paramref name="pos"/> in <paramref name="subject"/>,
|
|
/// or <see cref="NoPivot"/> if the position is at or beyond the end.
|
|
/// </summary>
|
|
internal static byte Pivot(byte[] subject, int pos)
|
|
=> pos >= subject.Length ? NoPivot : subject[pos];
|
|
|
|
/// <summary>
|
|
/// Computes the number of leading bytes that are equal between two spans.
|
|
/// </summary>
|
|
internal static int CommonPrefixLen(ReadOnlySpan<byte> s1, ReadOnlySpan<byte> s2)
|
|
{
|
|
var limit = Math.Min(s1.Length, s2.Length);
|
|
var i = 0;
|
|
while (i < limit && s1[i] == s2[i])
|
|
i++;
|
|
return i;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a copy of <paramref name="src"/>, or an empty array if src is empty.
|
|
/// </summary>
|
|
internal static byte[] CopyBytes(ReadOnlySpan<byte> src)
|
|
{
|
|
if (src.IsEmpty) return Array.Empty<byte>();
|
|
return src.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a copy of <paramref name="src"/>, or an empty array if src is null or empty.
|
|
/// </summary>
|
|
internal static byte[] CopyBytes(byte[]? src)
|
|
{
|
|
if (src == null || src.Length == 0) return Array.Empty<byte>();
|
|
var dst = new byte[src.Length];
|
|
src.CopyTo(dst, 0);
|
|
return dst;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal static string BytesToString(byte[] bytes)
|
|
{
|
|
if (bytes.Length == 0) return string.Empty;
|
|
return System.Text.Encoding.Latin1.GetString(bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal static byte[][] GenParts(byte[] filter)
|
|
{
|
|
var parts = new List<byte[]>(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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Matches parts against a fragment (prefix or suffix).
|
|
/// Returns the remaining parts and whether matching succeeded.
|
|
/// </summary>
|
|
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<byte[]>(), true);
|
|
return (parts[i..], true);
|
|
}
|
|
si = index + 1;
|
|
continue;
|
|
}
|
|
else if (part[0] == Fwc)
|
|
{
|
|
return (Array.Empty<byte[]>(), 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<byte[]>(), true);
|
|
|
|
si += part.Length;
|
|
}
|
|
|
|
return (parts, false);
|
|
}
|
|
}
|