From f56c9c39146110730ada86f2fe6098d493889931 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 21 Jul 2026 11:19:34 +0530 Subject: [PATCH 1/3] added case insensitive string comparison --- src/Condition.php | 35 ++++++++++++++++++++++++++++++----- tests/ConditionTest.php | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/Condition.php b/src/Condition.php index f42de31..7202006 100644 --- a/src/Condition.php +++ b/src/Condition.php @@ -421,6 +421,14 @@ private function matchesLogical(array $attributes): bool private function matchesEqual(mixed $value): bool { foreach ($this->values as $expected) { + if (\is_string($expected) && \is_string($value)) { + if (\strtolower($expected) === \strtolower($value)) { + return true; + } + + continue; + } + if ($expected === $value) { return true; } @@ -435,12 +443,29 @@ private function matchesEqual(mixed $value): bool private function matchesContains(mixed $value, array $needles): bool { if (\is_array($value)) { - return \count(array_intersect($value, $needles)) > 0; + foreach ($value as $item) { + foreach ($needles as $needle) { + if (\is_string($item) && \is_string($needle)) { + if (\strtolower($item) === \strtolower($needle)) { + return true; + } + + continue; + } + + if ($item === $needle) { + return true; + } + } + } + + return false; } if (\is_string($value)) { + $haystack = \strtolower($value); foreach ($needles as $needle) { - if (\is_string($needle) && $needle !== '' && str_contains($value, $needle)) { + if (\is_string($needle) && $needle !== '' && str_contains($haystack, \strtolower($needle))) { return true; } } @@ -481,7 +506,7 @@ private function matchesPrefix(mixed $value): bool return false; } - return str_starts_with($value, $prefix); + return str_starts_with(\strtolower($value), \strtolower($prefix)); } private function matchesSuffix(mixed $value): bool @@ -492,7 +517,7 @@ private function matchesSuffix(mixed $value): bool return false; } - return str_ends_with($value, $suffix); + return str_ends_with(\strtolower($value), \strtolower($suffix)); } /** @@ -542,7 +567,7 @@ private function compare(mixed $left, mixed $right): ?int } if (\is_string($left) && \is_string($right)) { - return $left <=> $right; + return \strtolower($left) <=> \strtolower($right); } if (\is_bool($left) && \is_bool($right)) { diff --git a/tests/ConditionTest.php b/tests/ConditionTest.php index aad0d25..455770e 100644 --- a/tests/ConditionTest.php +++ b/tests/ConditionTest.php @@ -170,6 +170,39 @@ public function testConditionSerializationRoundTrip(): void $this->assertFalse($parsed->matches(['ip' => '127.0.0.1', 'path' => '/web'])); } + public function testMatchingIsCaseInsensitive(): void + { + // equal: rule value and resolved attribute differ only in case. + $country = Condition::equal('country', ['in']); + $this->assertTrue($country->matches(['country' => 'IN'])); + $this->assertTrue($country->matches(['country' => 'In'])); + $this->assertFalse($country->matches(['country' => 'US'])); + + // notEqual inherits the same folding. + $notCountry = Condition::notEqual('country', 'in'); + $this->assertFalse($notCountry->matches(['country' => 'IN'])); + $this->assertTrue($notCountry->matches(['country' => 'US'])); + + // contains: both the string-haystack and array-membership forms fold case. + $stringContains = Condition::contains('userAgent', ['CURL']); + $this->assertTrue($stringContains->matches(['userAgent' => 'curl/8.4'])); + + $arrayContains = Condition::contains('tags', ['Security']); + $this->assertTrue($arrayContains->matches(['tags' => ['security', 'waf']])); + + // prefix / suffix fold case too. + $startsWith = Condition::startsWith('path', '/API'); + $this->assertTrue($startsWith->matches(['path' => '/api/v1'])); + + $endsWith = Condition::endsWith('path', '.JSON'); + $this->assertTrue($endsWith->matches(['path' => '/status.json'])); + + // Non-string values keep strict semantics (no accidental coercion). + $numeric = Condition::equal('count', [10]); + $this->assertTrue($numeric->matches(['count' => 10])); + $this->assertFalse($numeric->matches(['count' => '10'])); + } + public function testInvalidMethodThrowsException(): void { $this->expectException(ConditionException::class); From d16da766b8d2b69555f17f6e6114d18c57426472 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 21 Jul 2026 11:25:49 +0530 Subject: [PATCH 2/3] updated array membership --- src/Condition.php | 13 ++++--------- tests/ConditionTest.php | 5 +++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Condition.php b/src/Condition.php index 7202006..968b29c 100644 --- a/src/Condition.php +++ b/src/Condition.php @@ -443,17 +443,12 @@ private function matchesEqual(mixed $value): bool private function matchesContains(mixed $value, array $needles): bool { if (\is_array($value)) { + // Mirror the old array_intersect() semantics (scalars compared as + // strings, so 200 matches '200') while folding case. foreach ($value as $item) { foreach ($needles as $needle) { - if (\is_string($item) && \is_string($needle)) { - if (\strtolower($item) === \strtolower($needle)) { - return true; - } - - continue; - } - - if ($item === $needle) { + if (\is_scalar($item) && \is_scalar($needle) + && \strtolower((string) $item) === \strtolower((string) $needle)) { return true; } } diff --git a/tests/ConditionTest.php b/tests/ConditionTest.php index 455770e..69c4073 100644 --- a/tests/ConditionTest.php +++ b/tests/ConditionTest.php @@ -190,6 +190,11 @@ public function testMatchingIsCaseInsensitive(): void $arrayContains = Condition::contains('tags', ['Security']); $this->assertTrue($arrayContains->matches(['tags' => ['security', 'waf']])); + // Array membership keeps the old array_intersect() loose scalar + // semantics: an int needle still matches a numeric-string element. + $numericContains = Condition::contains('codes', [200]); + $this->assertTrue($numericContains->matches(['codes' => ['200', '404']])); + // prefix / suffix fold case too. $startsWith = Condition::startsWith('path', '/API'); $this->assertTrue($startsWith->matches(['path' => '/api/v1'])); From f32bf3b11c7bf14eb74a801424a531f8cb12e93d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 21 Jul 2026 11:32:27 +0530 Subject: [PATCH 3/3] keeping the case sensitiveness for comparsion --- src/Condition.php | 2 +- tests/ConditionTest.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Condition.php b/src/Condition.php index 968b29c..2731c9b 100644 --- a/src/Condition.php +++ b/src/Condition.php @@ -562,7 +562,7 @@ private function compare(mixed $left, mixed $right): ?int } if (\is_string($left) && \is_string($right)) { - return \strtolower($left) <=> \strtolower($right); + return $left <=> $right; } if (\is_bool($left) && \is_bool($right)) { diff --git a/tests/ConditionTest.php b/tests/ConditionTest.php index 69c4073..e6d0dfe 100644 --- a/tests/ConditionTest.php +++ b/tests/ConditionTest.php @@ -206,6 +206,12 @@ public function testMatchingIsCaseInsensitive(): void $numeric = Condition::equal('count', [10]); $this->assertTrue($numeric->matches(['count' => 10])); $this->assertFalse($numeric->matches(['count' => '10'])); + + // Ordering operators stay case-sensitive so string boundaries remain + // deterministic — only equality/substring matching folds case. + $between = Condition::between('name', 'BANANA', 'CHERRY'); + $this->assertFalse($between->matches(['name' => 'banana'])); + $this->assertTrue($between->matches(['name' => 'CANARY'])); } public function testInvalidMethodThrowsException(): void