108 lines
3.9 KiB
C#
108 lines
3.9 KiB
C#
namespace FluidExplorer.Services.Search.Usn;
|
|
|
|
/// <summary>
|
|
/// 通配符匹配器:<c>?</c> 匹配任意单字符,<c>*</c> 匹配任意长度(含空),
|
|
/// 大小写不敏感(用固定区域的大小写折叠,不产生任何分配、不受当前区域影响)。
|
|
///
|
|
/// 实现为经典的“双指针 + 最近星号回溯”,最坏 O(n*m),但对文件名这种短串是纳秒级。
|
|
/// </summary>
|
|
public static class WildcardMatcher
|
|
{
|
|
public static bool IsMatch(ReadOnlySpan<char> text, ReadOnlySpan<char> pattern)
|
|
{
|
|
// ---- 快路径:绝大多数真实查询是 "*.json" / "log*" / "*tmp*" 这类“只有一个/两个通配符”的模式,
|
|
// 直接退化成 EndsWith/StartsWith/IndexOf(走的是 BCL 的高度优化实现),比通用回溯快一个数量级。
|
|
int stars = 0, questions = 0, firstStar = -1, lastStar = -1;
|
|
for (int i = 0; i < pattern.Length; i++)
|
|
{
|
|
var c = pattern[i];
|
|
if (c == '*')
|
|
{
|
|
stars++;
|
|
if (firstStar < 0) firstStar = i;
|
|
lastStar = i;
|
|
}
|
|
else if (c == '?')
|
|
{
|
|
questions++;
|
|
}
|
|
}
|
|
|
|
if (questions == 0)
|
|
{
|
|
switch (stars)
|
|
{
|
|
case 0:
|
|
return text.Equals(pattern, StringComparison.OrdinalIgnoreCase);
|
|
case 1 when firstStar == 0:
|
|
return text.EndsWith(pattern[1..], StringComparison.OrdinalIgnoreCase);
|
|
case 1 when firstStar == pattern.Length - 1:
|
|
return text.StartsWith(pattern[..^1], StringComparison.OrdinalIgnoreCase);
|
|
case 2 when firstStar == 0 && lastStar == pattern.Length - 1:
|
|
return text.IndexOf(pattern[1..^1], StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
}
|
|
|
|
// ---- 通用路径:双指针 + 最近星号回溯 ----
|
|
int t = 0, p = 0;
|
|
int starPattern = -1;
|
|
int starText = 0;
|
|
|
|
while (t < text.Length)
|
|
{
|
|
if (p < pattern.Length && (pattern[p] == '?' || FoldEquals(pattern[p], text[t])))
|
|
{
|
|
t++;
|
|
p++;
|
|
}
|
|
else if (p < pattern.Length && pattern[p] == '*')
|
|
{
|
|
// 记下最近的星号位置,先当它匹配空串继续往前走
|
|
starPattern = p++;
|
|
starText = t;
|
|
}
|
|
else if (starPattern >= 0)
|
|
{
|
|
// 回溯:让最近的星号多吃一个字符
|
|
p = starPattern + 1;
|
|
t = ++starText;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
while (p < pattern.Length && pattern[p] == '*') p++;
|
|
return p == pattern.Length;
|
|
}
|
|
|
|
/// <summary>是否包含通配符。</summary>
|
|
public static bool HasWildcard(ReadOnlySpan<char> pattern) =>
|
|
pattern.IndexOfAny('*', '?') >= 0;
|
|
|
|
/// <summary>
|
|
/// 返回模式开头连续的字面量前缀(遇到 * 或 ? 为止),用于给通配符命中排优先级。
|
|
/// </summary>
|
|
public static ReadOnlySpan<char> LiteralPrefix(ReadOnlySpan<char> pattern)
|
|
{
|
|
int i = 0;
|
|
while (i < pattern.Length && pattern[i] is not ('*' or '?')) i++;
|
|
return pattern[..i];
|
|
}
|
|
|
|
/// <summary>区域无关的大小写折叠比较(只处理 ASCII 与 BMP 常见情形,足够文件名使用)。</summary>
|
|
internal static bool FoldEquals(char a, char b) =>
|
|
a == b || char.ToUpperInvariant(a) == char.ToUpperInvariant(b);
|
|
|
|
/// <summary>模式里是否只剩星号(即 "*" 或 "**" 这类恒真模式)。</summary>
|
|
internal static bool IsMatchAll(ReadOnlySpan<char> pattern)
|
|
{
|
|
foreach (var c in pattern)
|
|
{
|
|
if (c != '*') return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|