Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 134 additions & 86 deletions GameFrameX.Utility/IllegalWordDetection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
/// The algorithm idea comes from “http://www.cnblogs.com/sumtec/archive/2008/02/01/1061742.html”.
/// Performance: Average 2.7ms to detect and replace sensitive words in the test string.
/// </remarks>
public sealed class IllegalWordDetection

Check warning on line 47 in GameFrameX.Utility/IllegalWordDetection.cs

View workflow job for this annotation

GitHub Actions / analyze / quality-dotnet

Add a 'private' constructor or the 'static' keyword to the class declaration.
{
/// <summary>
/// 存储所有长度大于1的敏感词汇。
Expand Down Expand Up @@ -125,7 +125,7 @@
}
catch (Exception e)
{
LogHelper.Error<string>("IllegalWordDetection.Init {error}", LocalizationService.GetString(Localization.Keys.Utility.ExceptionLogs.ExceptionDetails, e.ToString()));

Check warning on line 128 in GameFrameX.Utility/IllegalWordDetection.cs

View workflow job for this annotation

GitHub Actions / analyze / quality-dotnet

Define a constant instead of using this literal 'IllegalWordDetection.Init {error}' 4 times.
}
});
}
Expand Down Expand Up @@ -264,57 +264,49 @@
return word;
}

private static unsafe bool RegisterBadWord(string badWord, ref int maxWordLength)
private static bool RegisterBadWord(string badWord, ref int maxWordLength)
{
var strBadWord = OriginalToLower(badWord);
//求得单个的敏感词汇的长度
var wordLength = strBadWord.Length;
maxWordLength = System.Math.Max(wordLength, maxWordLength);

fixed (char* pWordStart = strBadWord)
for (var i = 0; i < wordLength; ++i)
{
for (var i = 0; i < wordLength; ++i)
//准确记录8位以内的敏感词汇的某个词在词汇中的“位置”
if (i < 7)
{
//准确记录8位以内的敏感词汇的某个词在词汇中的“位置”
if (i < 7)
{
FastCheck[*(pWordStart + i)] |= (byte)(1 << i);
}
else //8位以外的敏感词汇的词直接限定在第8位
{
FastCheck[*(pWordStart + i)] |= 0x80; //0x80在内存中即为1000 0000,因为一个byte顶多标示8位,故超出8位的都位或上0x80,截断成第8位
}
FastCheck[strBadWord[i]] |= (byte)(1 << i);
}

//缓存敏感词汇的长度
var cachedWordsLength = System.Math.Min(8, wordLength);
var firstWord = *pWordStart;
//记录敏感词汇的“大致长度(超出8个字的敏感词汇会被截取成8的长度)”,“key”值为敏感词汇的第一个词
FastLength[firstWord] |= (byte)(1 << (cachedWordsLength - 1));
//缓存出当前以badWord第一个字开头的一系列的敏感词汇的最长的长度
if (StartCache[firstWord] < cachedWordsLength)
else //8位以外的敏感词汇的词直接限定在第8位
{
StartCache[firstWord] = (byte)cachedWordsLength;
FastCheck[strBadWord[i]] |= 0x80; //0x80在内存中即为1000 0000,因为一个byte顶多标示8位,故超出8位的都位或上0x80,截断成第8位
}
}

//存好敏感词汇的最后一个词汇的“出现情况”
EndCache[*(pWordStart + wordLength - 1)] = true;
//缓存敏感词汇的长度
var cachedWordsLength = System.Math.Min(8, wordLength);
var firstWord = strBadWord[0];
//记录敏感词汇的“大致长度(超出8个字的敏感词汇会被截取成8的长度)”,“key”值为敏感词汇的第一个词
FastLength[firstWord] |= (byte)(1 << (cachedWordsLength - 1));
//缓存出当前以badWord第一个字开头的一系列的敏感词汇的最长的长度
if (StartCache[firstWord] < cachedWordsLength)
{
StartCache[firstWord] = (byte)cachedWordsLength;
}

//存好敏感词汇的最后一个词汇的“出现情况”
EndCache[strBadWord[wordLength - 1]] = true;

//将长度大于1的敏感词汇都压入到字典中
return WordsSet.Add(strBadWord);
}

private static unsafe void InitSkipBitArray()
private static void InitSkipBitArray()
{
fixed (char* start = SkipList)
foreach (var c in SkipList)
{
var c = start;
var end = start + SkipList.Length;
while (c < end)
{
SkipBitArray[*c++] = true;
}
SkipBitArray[c] = true;
}
}

Expand Down Expand Up @@ -421,7 +413,7 @@
///// <param name="text"></param>
///// <returns></returns>
//unsafe public static bool IllegalWordsExistJudgement(string text)
//{

Check warning on line 416 in GameFrameX.Utility/IllegalWordDetection.cs

View workflow job for this annotation

GitHub Actions / analyze / quality-dotnet

Remove this commented out code.
// if (string.IsNullOrEmpty(text))
// return false;

Expand Down Expand Up @@ -500,7 +492,7 @@
/// <returns>如果包含敏感词汇返回 true,否则返回 false / True if the text contains sensitive words, false otherwise</returns>
public static bool HasBlockWords(string text)
{
return DetectIllegalWords(text, true, out var dic);

Check warning on line 495 in GameFrameX.Utility/IllegalWordDetection.cs

View workflow job for this annotation

GitHub Actions / analyze / quality-dotnet

Remove the unused local variable 'dic'.
}

/// <summary>
Expand Down Expand Up @@ -539,76 +531,132 @@

while (aitorIdx < textLength)
{
//如果text的第一个词不是敏感词汇或者当前遍历到了text第一个词的后面的词,则循环检测到text词汇的倒数第二个词,看看这一段子字符串中有没有敏感词汇
if ((FastCheck[text[aitorIdx]] & 0x01) == 0)
if (ScanFromPosition(text, textLength, ref aitorIdx, returnWhenFindFirst, findResult))
{
while (aitorIdx < textLength - 1 && (FastCheck[text[++aitorIdx]] & 0x01) == 0)
{
;
}
return true;
}
}

return findResult.Count > 0;
}

/// <summary>
/// 在主扫描循环的当前位置执行一次敏感词检测:推进游标到下一个候选首字符、记录单字符敏感词、向后扫描多字符敏感词。
/// 命中且需要首个即返回时返回 true;否则通过 ref 推进 aitorIdx 并返回 false。
/// </summary>
/// <remarks>
/// Performs one detection pass at the current cursor: advances to the next candidate start character,
/// records single-character bad words, then scans forward for multi-character bad words.
/// Returns true when a match is found and returnWhenFindFirst is set; otherwise advances aitorIdx via ref and returns false.
/// </remarks>
/// <param name="text">敏感词查询文本 / The text to check for sensitive words</param>
/// <param name="textLength">文本长度 / The text length</param>
/// <param name="aitorIdx">当前游标位置(引用),会被推进 / The current cursor (by ref), advanced by this call</param>
/// <param name="returnWhenFindFirst">是否找到第一个就返回 / Whether to return when the first match is found</param>
/// <param name="findResult">查找到的敏感词结果(键为起始位置,值为长度) / The found sensitive words (key is start position, value is length)</param>
/// <returns>是否找到敏感词并需立即返回 / Whether a match was found and the caller should return immediately</returns>
private static bool ScanFromPosition(string text, int textLength, ref int aitorIdx, bool returnWhenFindFirst, Dictionary<int, int> findResult)
{
aitorIdx = AdvanceToNextStartChar(text, textLength, aitorIdx);

//如果有只有一个词的敏感词,且当前的字符串的“非第一个词”满足这个敏感词,则先加入已检测到的敏感词列表
if (StartCache[text[aitorIdx]] != 0 && (FastLength[text[aitorIdx]] & 0x01) > 0)
//如果有只有一个词的敏感词,且当前的字符串的“非第一个词”满足这个敏感词,则先加入已检测到的敏感词列表
if (StartCache[text[aitorIdx]] != 0 && (FastLength[text[aitorIdx]] & 0x01) > 0)
{
//返回敏感词在text中的位置,以及敏感词的长度,供过滤功能用
findResult.Add(aitorIdx, 1);
if (returnWhenFindFirst)
{
//返回敏感词在text中的位置,以及敏感词的长度,供过滤功能用
findResult.Add(aitorIdx, 1);
if (returnWhenFindFirst)
{
return true;
}
return true;
}
}

//此时已经检测到一个敏感词的“首词”了,记录下第一个检测到的敏感词的位置
//从当前的位置检测到字符串末尾
if (TryScanForward(text, textLength, ref aitorIdx, findResult) && returnWhenFindFirst)
{
return true;
}

++aitorIdx;
Comment on lines +534 to +580

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

returnWhenFindFirstfalse 时,不要在首次多字符命中后跳过后续起始位置。

TryScanForward 在首次命中后把 aitorIdx 推进到匹配末尾。ScanFromPosition 随后再次递增该游标。此流程会跳过重叠敏感词。

例如,敏感词为 abbc,文本为 abc 时,当前代码只记录 abFilter 会产生 **c,而不会屏蔽 bc

仅当 returnWhenFindFirsttrue 时立即返回。否则,保持当前起始游标,并继续前向匹配和外层扫描。

建议修改
- if (TryScanForward(text, textLength, ref aitorIdx, findResult) && returnWhenFindFirst)
+ if (TryScanForward(text, textLength, aitorIdx, returnWhenFindFirst, findResult))
  {
      return true;
  }

- private static bool TryScanForward(string text, int textLength, ref int aitorIdx, Dictionary<int, int> findResult)
+ private static bool TryScanForward(string text, int textLength, int aitorIdx, bool returnWhenFindFirst, Dictionary<int, int> findResult)
  {
      // ...
      if (WordsSet.Contains(new string(_dectectedBuffer, 0, strIgorIdx)))
      {
          findResult[aitorIdx] = i + 1;
-         aitorIdx = subItoIdx;
-         return true;
+         if (returnWhenFindFirst)
+         {
+             return true;
+         }
      }
  }

Also applies to: 647-651

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GameFrameX.Utility/IllegalWordDetection.cs` around lines 534 - 580, Update
ScanFromPosition so that when TryScanForward finds a multi-character match and
returnWhenFindFirst is false, it does not leave aitorIdx at the match end or
increment past the next overlapping start; preserve the original scan start for
the outer loop while continuing forward matching. Keep the immediate return
behavior unchanged when returnWhenFindFirst is true, and apply the same cursor
handling to the corresponding logic near the other ScanFromPosition call site.

return false;
}

var strIgorIdx = 0;
_dectectedBuffer[strIgorIdx++] = text[aitorIdx];
var remainLength = textLength - aitorIdx - 1;
var skipCount = 0;
//此时已经检测到一个敏感词的“首词”了,记录下第一个检测到的敏感词的位置
//从当前的位置检测到字符串末尾
for (var i = 1; i <= remainLength; ++i)
/// <summary>
/// 跳过 FastCheck 标记为非敏感词首字符的位置,返回下一个候选首字符的索引。
/// </summary>
/// <remarks>
/// Skips characters not flagged by FastCheck as a sensitive-word start and returns the next candidate start index.
/// </remarks>
/// <param name="text">敏感词查询文本 / The text to check for sensitive words</param>
/// <param name="textLength">文本长度 / The text length</param>
/// <param name="aitorIdx">当前游标位置 / The current cursor</param>
/// <returns>推进后的候选首字符索引 / The advanced candidate start index</returns>
private static int AdvanceToNextStartChar(string text, int textLength, int aitorIdx)
{
//如果text的第一个词不是敏感词汇或者当前遍历到了text第一个词的后面的词,则循环检测到text词汇的倒数第二个词,看看这一段子字符串中有没有敏感词汇
if ((FastCheck[text[aitorIdx]] & 0x01) == 0)
{
while (aitorIdx < textLength - 1 && (FastCheck[text[++aitorIdx]] & 0x01) == 0)
{

Check warning on line 600 in GameFrameX.Utility/IllegalWordDetection.cs

View workflow job for this annotation

GitHub Actions / analyze / quality-dotnet

Either remove or fill this block of code.
var subItoIdx = aitorIdx + i;
// 跳过一些过滤的字符,比如空格特殊符号之类的
if (SkipBitArray[text[subItoIdx]])
{
++skipCount;
continue;
}
}
}

//如果检测到当前的词在所有敏感词中的位置信息中没有处在第i位的,则马上跳出遍历
if (FastCheck[text[subItoIdx]] >> System.Math.Min(i - skipCount, 7) == 0)
{
break;
}
return aitorIdx;
}

_dectectedBuffer[strIgorIdx++] = text[subItoIdx];
//如果有检测到敏感词的最后一个词,并且此时的“检测到的敏感词汇”的长度也符合要求,则才进一步查看检测到的敏感词汇是否是真的敏感
if (FastLength[text[aitorIdx]] >> System.Math.Min(i - 1 - skipCount, 7) > 0 && EndCache[text[subItoIdx]])
{
//如果此子字符串在敏感词字典中存在,则记录。做此判断是避免敏感词中夹杂了其他敏感词的单词,而上面的算法无法剔除,故先用hash数组来剔除
//上述算法是用于减少大部分的比较消耗
if (WordsSet.Contains(new string(_dectectedBuffer, 0, strIgorIdx)))
{
findResult[aitorIdx] = i + 1;
aitorIdx = subItoIdx;
/// <summary>
/// 从当前首字符向后扫描多字符敏感词。命中时把 aitorIdx 推进到匹配末字符并返回 true,否则返回 false。
/// </summary>
/// <remarks>
/// Scans forward from the current start character for multi-character sensitive words.
/// On a match, advances aitorIdx to the matched last character via ref and returns true; otherwise returns false.
/// </remarks>
/// <param name="text">敏感词查询文本 / The text to check for sensitive words</param>
/// <param name="textLength">文本长度 / The text length</param>
/// <param name="aitorIdx">当前游标位置(引用),命中时推进到匹配末字符 / The current cursor (by ref), advanced to the matched last character on hit</param>
/// <param name="findResult">查找到的敏感词结果(键为起始位置,值为长度) / The found sensitive words (key is start position, value is length)</param>
/// <returns>是否命中多字符敏感词 / Whether a multi-character sensitive word was matched</returns>
private static bool TryScanForward(string text, int textLength, ref int aitorIdx, Dictionary<int, int> findResult)
{
var strIgorIdx = 0;
_dectectedBuffer[strIgorIdx++] = text[aitorIdx];
var remainLength = textLength - aitorIdx - 1;
var skipCount = 0;
for (var i = 1; i <= remainLength; ++i)
{
var subItoIdx = aitorIdx + i;
// 跳过一些过滤的字符,比如空格特殊符号之类的
if (SkipBitArray[text[subItoIdx]])
{
++skipCount;
continue;
}

if (returnWhenFindFirst)
{
return true;
}
//如果检测到当前的词在所有敏感词中的位置信息中没有处在第i位的,则马上跳出遍历
if (FastCheck[text[subItoIdx]] >> System.Math.Min(i - skipCount, 7) == 0)
{
break;
}

break;
}
}
else if (i - skipCount > StartCache[text[aitorIdx]] && StartCache[text[aitorIdx]] < 0x80) //如果超过了以该词为首的一系列的敏感词汇的最大的长度,则不继续判断(前提是该词对应的所有敏感词汇没有超过8个词的)
_dectectedBuffer[strIgorIdx++] = text[subItoIdx];
//如果有检测到敏感词的最后一个词,并且此时的“检测到的敏感词汇”的长度也符合要求,则才进一步查看检测到的敏感词汇是否是真的敏感
if (FastLength[text[aitorIdx]] >> System.Math.Min(i - 1 - skipCount, 7) > 0 && EndCache[text[subItoIdx]])
{
//如果此子字符串在敏感词字典中存在,则记录。做此判断是避免敏感词中夹杂了其他敏感词的单词,而上面的算法无法剔除,故先用hash数组来剔除
//上述算法是用于减少大部分的比较消耗
if (WordsSet.Contains(new string(_dectectedBuffer, 0, strIgorIdx)))
{
break;
findResult[aitorIdx] = i + 1;
aitorIdx = subItoIdx;
return true;
}
}

++aitorIdx;
else if (i - skipCount > StartCache[text[aitorIdx]] && StartCache[text[aitorIdx]] < 0x80) //如果超过了以该词为首的一系列的敏感词汇的最大的长度,则不继续判断(前提是该词对应的所有敏感词汇没有超过8个词的)
{
break;
}
}

return findResult.Count > 0;
return false;
}
}
Loading