feat: add NumTokens, TokenAt, SubjectsCollide, UTF-8 validation to SubjectMatch
This commit is contained in:
@@ -113,4 +113,112 @@ public static class SubjectMatch
|
|||||||
|
|
||||||
return li >= literal.Length; // both exhausted
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,50 @@ public class SubjectMatchTests
|
|||||||
{
|
{
|
||||||
SubjectMatch.MatchLiteral(literal, pattern).ShouldBe(expected);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user