Skip to content

Commit a03f977

Browse files
deemonicclaude
andcommitted
fix: address remaining CodeRabbit review findings on PR #48
- RegexDriver: track masked ranges and use immutable normalized string for position lookups to prevent offset drift across mutated buffers - RegexDriver: extract matched text from original input instead of normalized string to preserve original casing/characters - PipelineDriver: pass explicit UTF-8 encoding to mb_substr calls - PendingCheck: cap cache key tracking with configurable max_tracked_keys to prevent unbounded growth - French config: remove mince/flûte/flute from profanity list as they are common benign words that cause false positives - German config: move schwul/schwule/schwuler/schwules from extreme to moderate severity as they are also neutral self-identifiers - StrMacroTest: replace toString() with (string) cast for Laravel 8 compat Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2728501 commit a03f977

6 files changed

Lines changed: 43 additions & 13 deletions

File tree

‎config/languages/french.php‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
'severity' => [
55
'mild' => [
66
'crotte', 'crottes', 'caca', 'cacas', 'zut',
7-
'mince', 'flûte', 'flute', 'punaise',
7+
'punaise',
88
'idiot', 'idiots', 'idiote', 'idiotes',
99
'bête', 'bete', 'bêtes', 'betes',
1010
'sot', 'sots', 'sotte', 'sottes',
@@ -1537,9 +1537,6 @@
15371537
'réfrigérations',
15381538
'refrigerations',
15391539
'zut',
1540-
'mince',
1541-
'flûte',
1542-
'flute',
15431540
'punaise',
15441541
],
15451542

‎config/languages/german.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
'bekloppt', 'bekloppte', 'bekloppter', 'beklopptes',
2020
'schwanz', 'pimmel',
2121
'hintern', 'po', 'popo',
22+
'schwul', 'schwuler', 'schwule', 'schwules',
2223
],
2324
'high' => [
2425
'scheiße', 'scheisse', 'ficken', 'fick', 'gefickt',
@@ -27,7 +28,6 @@
2728
'vögeln', 'voegeln', 'bumsen',
2829
],
2930
'extreme' => [
30-
'schwul', 'schwuler', 'schwule', 'schwules',
3131
'tunte', 'tuntig',
3232
'kampflesbe', 'kampflesben',
3333
'kanake', 'kanaken',

‎src/Drivers/PipelineDriver.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function detect(string $text, Dictionary $dictionary, MaskStrategyInterfa
6565
$reversed = array_reverse($kept);
6666
foreach ($reversed as $match) {
6767
$replacement = $mask->mask($match->text, $match->length);
68-
$cleanText = mb_substr($cleanText, 0, $match->position) . $replacement . mb_substr($cleanText, $match->position + $match->length);
68+
$cleanText = mb_substr($cleanText, 0, $match->position, 'UTF-8') . $replacement . mb_substr($cleanText, $match->position + $match->length, null, 'UTF-8');
6969
}
7070

7171
// 5. Recalculate score from merged matches

‎src/Drivers/RegexDriver.php‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,17 @@ public function detect(string $text, Dictionary $dictionary, MaskStrategyInterfa
3939
$normalizedString = $normalizer->normalize($text);
4040
$originalNormalized = preg_replace('/\s+/', ' ', $normalizedString);
4141

42+
// Immutable copy for position lookups — never mutated
43+
$immutableNormalized = $originalNormalized;
44+
4245
$matchedWords = [];
4346
$uniqueMap = [];
4447
$profanitiesCount = 0;
4548
$continue = true;
4649

50+
// Track masked character ranges so we don't re-match them
51+
$maskedRanges = [];
52+
4753
while ($continue) {
4854
$continue = false;
4955
$normalizedString = preg_replace('/\s+/', ' ', $normalizedString);
@@ -59,6 +65,19 @@ public function detect(string $text, Dictionary $dictionary, MaskStrategyInterfa
5965
$length = mb_strlen($match[0], 'UTF-8');
6066
$matchedText = $match[0];
6167

68+
// Skip if this range overlaps with an already-masked range
69+
$matchEnd = $start + $length;
70+
$alreadyMasked = false;
71+
foreach ($maskedRanges as [$mStart, $mEnd]) {
72+
if ($start < $mEnd && $matchEnd > $mStart) {
73+
$alreadyMasked = true;
74+
break;
75+
}
76+
}
77+
if ($alreadyMasked) {
78+
continue;
79+
}
80+
6281
// Check word boundary spanning (filter uses byte-level operations)
6382
if ($this->filter->isSpanningWordBoundary($matchedText, $normalizedString, $byteStart)) {
6483
continue;
@@ -73,7 +92,7 @@ public function detect(string $text, Dictionary $dictionary, MaskStrategyInterfa
7392
$fullWord = $this->filter->getFullWordContext($normalizedString, $byteStart, $byteLength);
7493

7594
// Check pure alpha substring against original (unmasked) normalized
76-
$originalFullWord = $this->filter->getFullWordContext($originalNormalized, $byteStart, $byteLength);
95+
$originalFullWord = $this->filter->getFullWordContext($immutableNormalized, $byteStart, $byteLength);
7796
if ($this->compoundDetector->isPureAlphaSubstring($matchedText, $originalFullWord, $profanity, $profanityExpressions)) {
7897
continue;
7998
}
@@ -86,14 +105,20 @@ public function detect(string $text, Dictionary $dictionary, MaskStrategyInterfa
86105
$continue = true;
87106

88107
// Mask in normalizedString only (needed for loop termination)
89-
$normalizedString = mb_substr($normalizedString, 0, $start) . str_repeat('*', mb_strlen($match[0], 'UTF-8')) .
90-
mb_substr($normalizedString, $start + mb_strlen($match[0], 'UTF-8'));
108+
$normalizedString = mb_substr($normalizedString, 0, $start) . str_repeat('*', $length) .
109+
mb_substr($normalizedString, $start + $length);
91110

92-
// Track match
111+
// Record masked range using character positions from immutable string
112+
$maskedRanges[] = [$start, $matchEnd];
113+
114+
// Track match — use position derived from immutable normalized string
93115
$profanitiesCount++;
94116

117+
// Get the original text at this position from the original input
118+
$originalMatchText = mb_substr($text, $start, $length);
119+
95120
$matchedWords[] = new MatchedWord(
96-
text: $matchedText,
121+
text: $originalMatchText,
97122
base: $profanity,
98123
severity: $dictionary->getSeverity($profanity),
99124
position: $start,

‎src/PendingCheck.php‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,14 @@ protected function trackCacheKey(string $key): void
318318
$cache = $this->getCache();
319319
$keys = $cache->get('blasp_result_cache_keys', []);
320320
$keys[] = $key;
321-
$cache->forever('blasp_result_cache_keys', array_unique($keys));
321+
$keys = array_unique($keys);
322+
323+
// Evict oldest keys when exceeding the configured limit
324+
$maxKeys = config('blasp.cache.max_tracked_keys', 1000);
325+
if (count($keys) > $maxKeys) {
326+
$keys = array_slice($keys, -$maxKeys);
327+
}
328+
329+
$cache->forever('blasp_result_cache_keys', $keys);
322330
}
323331
}

‎tests/StrMacroTest.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,6 @@ public function test_stringable_clean_profanity_returns_stringable_instance()
5151

5252
public function test_stringable_clean_profanity_returns_clean_text_unchanged()
5353
{
54-
$this->assertSame('hello', Str::of('hello')->cleanProfanity()->toString());
54+
$this->assertSame('hello', (string) Str::of('hello')->cleanProfanity());
5555
}
5656
}

0 commit comments

Comments
 (0)