Files
natsdotnet/src/NATS.Server/Internal/SubjectTree/Parts.cs
Joseph Doherty 256daad8e5 feat: port internal data structures from Go (Wave 2)
- AVL SequenceSet: sparse sequence set with AVL tree, 16 tests
- Subject Tree: Adaptive Radix Tree (ART) with 5 node tiers, 59 tests
- Generic Subject List: trie-based subject matcher, 21 tests
- Time Hash Wheel: O(1) TTL expiration wheel, 8 tests

Total: 106 new tests (1,081 → 1,187 passing)
2026-02-23 20:56:20 -05:00

244 lines
7.5 KiB
C#

// Go reference: server/stree/parts.go, server/stree/util.go
namespace NATS.Server.Internal.SubjectTree;
/// <summary>
/// Subject tokenization helpers and match logic for the ART.
/// </summary>
internal static class Parts
{
// For subject matching.
internal const byte Pwc = (byte)'*';
internal const byte Fwc = (byte)'>';
internal const byte Tsep = (byte)'.';
/// <summary>
/// No pivot available sentinel value (DEL character).
/// </summary>
internal const byte NoPivot = 127;
/// <summary>
/// Returns the pivot byte at the given position, or NoPivot if past end.
/// Go reference: server/stree/util.go:pivot
/// </summary>
internal static byte Pivot(ReadOnlySpan<byte> subject, int pos)
{
if (pos >= subject.Length) return NoPivot;
return subject[pos];
}
/// <summary>
/// Returns the length of the common prefix between two byte spans.
/// Go reference: server/stree/util.go:commonPrefixLen
/// </summary>
internal static int CommonPrefixLen(ReadOnlySpan<byte> s1, ReadOnlySpan<byte> s2)
{
var limit = Math.Min(s1.Length, s2.Length);
int i = 0;
for (; i < limit; i++)
{
if (s1[i] != s2[i]) break;
}
return i;
}
/// <summary>
/// Copy bytes helper.
/// </summary>
internal static byte[] CopyBytes(ReadOnlySpan<byte> src)
{
if (src.Length == 0) return [];
return src.ToArray();
}
/// <summary>
/// Break a filter subject into parts based on wildcards (pwc '*' and fwc '>').
/// Go reference: server/stree/parts.go:genParts
/// </summary>
internal static ReadOnlyMemory<byte>[] GenParts(ReadOnlySpan<byte> filter)
{
var parts = new List<ReadOnlyMemory<byte>>();
// We work on a copy since ReadOnlyMemory needs a backing array
var filterArr = filter.ToArray();
var filterMem = new ReadOnlyMemory<byte>(filterArr);
int start = 0;
int e = filterArr.Length - 1;
for (int i = 0; i < filterArr.Length; i++)
{
if (filterArr[i] == Tsep)
{
// See if next token is pwc. Either internal or end pwc.
if (i < e && filterArr[i + 1] == Pwc && ((i + 2 <= e && filterArr[i + 2] == Tsep) || i + 1 == e))
{
if (i > start)
{
parts.Add(filterMem.Slice(start, i + 1 - start));
}
parts.Add(filterMem.Slice(i + 1, 1));
i++; // Skip pwc
if (i + 2 <= e)
{
i++; // Skip next tsep from next part too.
}
start = i + 1;
}
else if (i < e && filterArr[i + 1] == Fwc && i + 1 == e)
{
if (i > start)
{
parts.Add(filterMem.Slice(start, i + 1 - start));
}
parts.Add(filterMem.Slice(i + 1, 1));
i++; // Skip fwc
start = i + 1;
}
}
else if (filterArr[i] == Pwc || filterArr[i] == Fwc)
{
// Wildcard must be at the start or preceded by tsep.
int prev = i - 1;
if (prev >= 0 && filterArr[prev] != Tsep)
{
continue;
}
// Wildcard must be at the end or followed by tsep.
int next = i + 1;
if (next == e || (next < e && filterArr[next] != Tsep))
{
continue;
}
// Full wildcard must be terminal.
if (filterArr[i] == Fwc && i < e)
{
break;
}
// We start with a pwc or fwc.
parts.Add(filterMem.Slice(i, 1));
if (i + 1 <= e)
{
i++; // Skip next tsep from next part too.
}
start = i + 1;
}
}
if (start < filterArr.Length)
{
// Check to see if we need to eat a leading tsep.
if (filterArr[start] == Tsep)
{
start++;
}
parts.Add(filterMem[start..]);
}
return [.. parts];
}
/// <summary>
/// Match parts against a fragment (prefix for nodes or suffix for leaves).
/// Go reference: server/stree/parts.go:matchParts
/// </summary>
internal static (ReadOnlyMemory<byte>[] RemainingParts, bool Matched) MatchPartsAgainstFragment(
ReadOnlyMemory<byte>[] parts, ReadOnlySpan<byte> frag)
{
int lf = frag.Length;
if (lf == 0)
{
return (parts, true);
}
int si = 0;
int lpi = parts.Length - 1;
for (int i = 0; i < parts.Length; i++)
{
if (si >= lf)
{
return (parts[i..], true);
}
var part = parts[i].Span;
int lp = part.Length;
// Check for pwc or fwc place holders.
if (lp == 1)
{
if (part[0] == Pwc)
{
var index = frag[si..].IndexOf(Tsep);
// We are trying to match pwc and did not find our tsep.
if (index < 0)
{
if (i == lpi)
{
return ([], true);
}
return (parts[i..], true);
}
si += index + 1;
continue;
}
else if (part[0] == Fwc)
{
return ([], true);
}
}
int end = Math.Min(si + lp, lf);
// If part is bigger than the remaining fragment, adjust to a portion of the part.
var partToCompare = part;
if (si + lp > end)
{
// Frag is smaller than part itself.
partToCompare = part[..(end - si)];
}
if (!partToCompare.SequenceEqual(frag[si..end]))
{
return (parts, false);
}
// If we still have a portion of the fragment left, update and continue.
if (end < lf)
{
si = end;
continue;
}
// If we matched a partial, do not move past current part
// but update the part to what was consumed.
if (end < si + lp)
{
if (end >= lf)
{
// Create a copy with the current part trimmed.
var newParts = new ReadOnlyMemory<byte>[parts.Length - i];
Array.Copy(parts, i, newParts, 0, newParts.Length);
newParts[0] = parts[i][(lf - si)..];
return (newParts, true);
}
else
{
i++;
}
return (parts[i..], true);
}
if (i == lpi)
{
return ([], true);
}
// If we are here we are not the last part which means we have a wildcard
// gap, so we need to match anything up to next tsep.
si += part.Length;
}
return (parts, false);
}
}