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
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
[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