feat: add NumTokens, TokenAt, SubjectsCollide, UTF-8 validation to SubjectMatch

This commit is contained in:
Joseph Doherty
2026-02-23 00:33:43 -05:00
parent e87d4c00d9
commit dddced444e
2 changed files with 154 additions and 0 deletions

View File

@@ -113,4 +113,112 @@ public static class SubjectMatch
return li >= literal.Length; // both exhausted
}
/// <summary>Count dot-delimited tokens. Empty string returns 0.</summary>
public static int NumTokens(string subject)
{
if (string.IsNullOrEmpty(subject))
return 0;
int count = 1;
for (int i = 0; i < subject.Length; i++)
{
if (subject[i] == Sep)
count++;
}
return count;
}
/// <summary>Return the 0-based nth token as a span. Returns empty if out of range.</summary>
public static ReadOnlySpan<char> TokenAt(string subject, int index)
{
if (string.IsNullOrEmpty(subject))
return default;
var span = subject.AsSpan();
int current = 0;
int start = 0;
for (int i = 0; i < span.Length; i++)
{
if (span[i] == Sep)
{
if (current == index)
return span[start..i];
start = i + 1;
current++;
}
}
if (current == index)
return span[start..];
return default;
}
/// <summary>
/// Determines if two subject patterns (possibly containing wildcards) can both
/// match the same literal subject. Reference: Go sublist.go SubjectsCollide.
/// </summary>
public static bool SubjectsCollide(string subj1, string subj2)
{
if (subj1 == subj2)
return true;
bool lit1 = IsLiteral(subj1);
bool lit2 = IsLiteral(subj2);
if (lit1 && lit2)
return false;
if (lit1 && !lit2)
return MatchLiteral(subj1, subj2);
if (lit2 && !lit1)
return MatchLiteral(subj2, subj1);
// Both have wildcards
int n1 = NumTokens(subj1);
int n2 = NumTokens(subj2);
bool hasFwc1 = subj1.Contains('>');
bool hasFwc2 = subj2.Contains('>');
if (!hasFwc1 && !hasFwc2 && n1 != n2)
return false;
if (n1 < n2 && !hasFwc1)
return false;
if (n2 < n1 && !hasFwc2)
return false;
int stop = Math.Min(n1, n2);
for (int i = 0; i < stop; i++)
{
var t1 = TokenAt(subj1, i);
var t2 = TokenAt(subj2, i);
if (!TokensCanMatch(t1, t2))
return false;
}
return true;
}
private static bool TokensCanMatch(ReadOnlySpan<char> t1, ReadOnlySpan<char> t2)
{
if (t1.Length == 1 && (t1[0] == Pwc || t1[0] == Fwc))
return true;
if (t2.Length == 1 && (t2[0] == Pwc || t2[0] == Fwc))
return true;
return t1.SequenceEqual(t2);
}
/// <summary>
/// Validates subject. When checkRunes is true, also rejects null bytes.
/// </summary>
public static bool IsValidSubject(string subject, bool checkRunes)
{
if (!IsValidSubject(subject))
return false;
if (!checkRunes)
return true;
for (int i = 0; i < subject.Length; i++)
{
if (subject[i] == '\0')
return false;
}
return true;
}
}

View File

@@ -54,4 +54,50 @@ public class SubjectMatchTests
{
SubjectMatch.MatchLiteral(literal, pattern).ShouldBe(expected);
}
[Theory]
[InlineData("foo.bar.baz", 3)]
[InlineData("foo", 1)]
[InlineData("a.b.c.d.e", 5)]
[InlineData("", 0)]
public void NumTokens(string subject, int expected)
{
SubjectMatch.NumTokens(subject).ShouldBe(expected);
}
[Theory]
[InlineData("foo.bar.baz", 0, "foo")]
[InlineData("foo.bar.baz", 1, "bar")]
[InlineData("foo.bar.baz", 2, "baz")]
[InlineData("foo", 0, "foo")]
[InlineData("foo.bar.baz", 5, "")]
public void TokenAt(string subject, int index, string expected)
{
SubjectMatch.TokenAt(subject, index).ToString().ShouldBe(expected);
}
[Theory]
[InlineData("foo.bar", "foo.bar", true)]
[InlineData("foo.bar", "foo.baz", false)]
[InlineData("foo.*", "foo.bar", true)]
[InlineData("foo.*", "foo.>", true)]
[InlineData("foo.>", "foo.bar.baz", true)]
[InlineData(">", "foo.bar", true)]
[InlineData("foo.*", "bar.*", false)]
[InlineData("foo.*.baz", "foo.bar.*", true)]
[InlineData("*.bar", "foo.*", true)]
[InlineData("foo.*", "bar.>", false)]
public void SubjectsCollide(string subj1, string subj2, bool expected)
{
SubjectMatch.SubjectsCollide(subj1, subj2).ShouldBe(expected);
}
[Theory]
[InlineData("foo\0bar", true, false)]
[InlineData("foo\0bar", false, true)]
[InlineData("foo.bar", true, true)]
public void IsValidSubject_checkRunes(string subject, bool checkRunes, bool expected)
{
SubjectMatch.IsValidSubject(subject, checkRunes).ShouldBe(expected);
}
}