feat: add Subscription types and subject validation with wildcard matching
This commit is contained in:
15
src/NATS.Server/Subscriptions/SubListResult.cs
Normal file
15
src/NATS.Server/Subscriptions/SubListResult.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace NATS.Server.Subscriptions;
|
||||
|
||||
public sealed class SubListResult
|
||||
{
|
||||
public static readonly SubListResult Empty = new([], []);
|
||||
|
||||
public Subscription[] PlainSubs { get; }
|
||||
public Subscription[][] QueueSubs { get; } // outer = groups, inner = members
|
||||
|
||||
public SubListResult(Subscription[] plainSubs, Subscription[][] queueSubs)
|
||||
{
|
||||
PlainSubs = plainSubs;
|
||||
QueueSubs = queueSubs;
|
||||
}
|
||||
}
|
||||
116
src/NATS.Server/Subscriptions/SubjectMatch.cs
Normal file
116
src/NATS.Server/Subscriptions/SubjectMatch.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
namespace NATS.Server.Subscriptions;
|
||||
|
||||
public static class SubjectMatch
|
||||
{
|
||||
public const char Pwc = '*'; // partial wildcard
|
||||
public const char Fwc = '>'; // full wildcard
|
||||
public const char Sep = '.'; // token separator
|
||||
|
||||
public static bool IsValidSubject(string subject)
|
||||
{
|
||||
if (string.IsNullOrEmpty(subject))
|
||||
return false;
|
||||
|
||||
bool sawFwc = false;
|
||||
int start = 0;
|
||||
|
||||
for (int i = 0; i <= subject.Length; i++)
|
||||
{
|
||||
if (i == subject.Length || subject[i] == Sep)
|
||||
{
|
||||
int tokenLen = i - start;
|
||||
if (tokenLen == 0 || sawFwc)
|
||||
return false;
|
||||
|
||||
if (tokenLen == 1)
|
||||
{
|
||||
char c = subject[start];
|
||||
if (c == Fwc)
|
||||
sawFwc = true;
|
||||
else if (c is ' ' or '\t' or '\n' or '\r' or '\f')
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int j = start; j < i; j++)
|
||||
{
|
||||
char c = subject[j];
|
||||
if (c is ' ' or '\t' or '\n' or '\r' or '\f')
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsLiteral(string subject)
|
||||
{
|
||||
for (int i = 0; i < subject.Length; i++)
|
||||
{
|
||||
char c = subject[i];
|
||||
if (c is Pwc or Fwc)
|
||||
{
|
||||
bool atStart = i == 0 || subject[i - 1] == Sep;
|
||||
bool atEnd = i + 1 == subject.Length || subject[i + 1] == Sep;
|
||||
if (atStart && atEnd)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsValidPublishSubject(string subject)
|
||||
{
|
||||
return IsValidSubject(subject) && IsLiteral(subject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match a literal subject against a pattern that may contain wildcards.
|
||||
/// </summary>
|
||||
public static bool MatchLiteral(string literal, string pattern)
|
||||
{
|
||||
int li = 0, pi = 0;
|
||||
|
||||
while (pi < pattern.Length)
|
||||
{
|
||||
// Get next pattern token
|
||||
int pTokenStart = pi;
|
||||
while (pi < pattern.Length && pattern[pi] != Sep)
|
||||
pi++;
|
||||
int pTokenLen = pi - pTokenStart;
|
||||
if (pi < pattern.Length)
|
||||
pi++; // skip separator
|
||||
|
||||
// Full wildcard -- matches everything remaining
|
||||
if (pTokenLen == 1 && pattern[pTokenStart] == Fwc)
|
||||
return li < literal.Length; // must have at least one token left
|
||||
|
||||
// Get next literal token
|
||||
if (li >= literal.Length)
|
||||
return false;
|
||||
int lTokenStart = li;
|
||||
while (li < literal.Length && literal[li] != Sep)
|
||||
li++;
|
||||
int lTokenLen = li - lTokenStart;
|
||||
if (li < literal.Length)
|
||||
li++; // skip separator
|
||||
|
||||
// Partial wildcard -- matches any single token
|
||||
if (pTokenLen == 1 && pattern[pTokenStart] == Pwc)
|
||||
continue;
|
||||
|
||||
// Literal comparison
|
||||
if (pTokenLen != lTokenLen)
|
||||
return false;
|
||||
if (string.Compare(literal, lTokenStart, pattern, pTokenStart, pTokenLen, StringComparison.Ordinal) != 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return li >= literal.Length; // both exhausted
|
||||
}
|
||||
}
|
||||
10
src/NATS.Server/Subscriptions/Subscription.cs
Normal file
10
src/NATS.Server/Subscriptions/Subscription.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace NATS.Server.Subscriptions;
|
||||
|
||||
public sealed class Subscription
|
||||
{
|
||||
public required string Subject { get; init; }
|
||||
public string? Queue { get; init; }
|
||||
public required string Sid { get; init; }
|
||||
public long MessageCount; // Interlocked
|
||||
public long MaxMessages; // 0 = unlimited
|
||||
}
|
||||
57
tests/NATS.Server.Tests/SubjectMatchTests.cs
Normal file
57
tests/NATS.Server.Tests/SubjectMatchTests.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class SubjectMatchTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("foo", true)]
|
||||
[InlineData("foo.bar", true)]
|
||||
[InlineData("foo.bar.baz", true)]
|
||||
[InlineData("foo.*", true)]
|
||||
[InlineData("foo.>", true)]
|
||||
[InlineData(">", true)]
|
||||
[InlineData("*", true)]
|
||||
[InlineData("*.bar", true)]
|
||||
[InlineData("foo.*.baz", true)]
|
||||
[InlineData("", false)]
|
||||
[InlineData("foo.", false)]
|
||||
[InlineData(".foo", false)]
|
||||
[InlineData("foo..bar", false)]
|
||||
[InlineData("foo.>.bar", false)] // > must be last token
|
||||
[InlineData("foo bar", false)] // no spaces
|
||||
[InlineData("foo\tbar", false)] // no tabs
|
||||
public void IsValidSubject(string subject, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, SubjectMatch.IsValidSubject(subject));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("foo", true)]
|
||||
[InlineData("foo.bar.baz", true)]
|
||||
[InlineData("foo.*", false)]
|
||||
[InlineData("foo.>", false)]
|
||||
[InlineData(">", false)]
|
||||
[InlineData("*", false)]
|
||||
public void IsValidPublishSubject(string subject, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, SubjectMatch.IsValidPublishSubject(subject));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("foo", "foo", true)]
|
||||
[InlineData("foo", "bar", false)]
|
||||
[InlineData("foo.bar", "foo.*", true)]
|
||||
[InlineData("foo.bar", "*.bar", true)]
|
||||
[InlineData("foo.bar", "*.*", true)]
|
||||
[InlineData("foo.bar.baz", "foo.>", true)]
|
||||
[InlineData("foo.bar.baz", ">", true)]
|
||||
[InlineData("foo.bar", "foo.>", true)]
|
||||
[InlineData("foo", "foo.>", false)]
|
||||
[InlineData("foo.bar.baz", "foo.*", false)]
|
||||
[InlineData("foo.bar", "foo.bar.>", false)]
|
||||
public void MatchLiteral(string literal, string pattern, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, SubjectMatch.MatchLiteral(literal, pattern));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user