diff --git a/4dev/tests/Combined/CoreLibsCombinedArrayHandlerFindArraysMissingKeyTest.php b/4dev/tests/Combined/CoreLibsCombinedArrayHandlerFindArraysMissingKeyTest.php index 1bde4bad..9553c0af 100644 --- a/4dev/tests/Combined/CoreLibsCombinedArrayHandlerFindArraysMissingKeyTest.php +++ b/4dev/tests/Combined/CoreLibsCombinedArrayHandlerFindArraysMissingKeyTest.php @@ -1,5 +1,7 @@ this is valid +// '/test{1,}/', // Invalid quantifier -> this is valid + +declare(strict_types=1); + +namespace tests; + +use PHPUnit\Framework\TestCase; +use CoreLibs\Convert\Strings; + +/** + * Test class for CoreLibs\Convert\Strings regex validation methods + */ +class CoreLibsConvertStringsRegexValidateTest extends TestCase +{ + /** + * Test isValidRegex with valid regex patterns + */ + public function testIsValidRegexWithValidPatterns(): void + { + $validPatterns = [ + '/^[a-zA-Z0-9]+$/', + '/test/', + '/\d+/', + '/^hello.*world$/', + '/[0-9]{3}-[0-9]{3}-[0-9]{4}/', + '#^https?://.*#i', + '~^[a-z]+~', + '|test|', + '/^$/m', + '/\w+/u', + ]; + + foreach ($validPatterns as $pattern) { + $this->assertTrue( + Strings::isValidRegex($pattern), + "Pattern '{$pattern}' should be valid" + ); + } + } + + /** + * Test isValidRegex with invalid regex patterns + */ + public function testIsValidRegexWithInvalidPatterns(): void + { + $invalidPatterns = [ + '/[/', // Unmatched bracket + '/test[/', // Unmatched bracket + '/(?P/', // Unmatched parenthesis + '/(?P<>test)/', // Invalid named group + '/test\\/', // Invalid escape at end + '/(test/', // Unmatched parenthesis + '/test)/', // Unmatched parenthesis + // '/test{/', // Unmatched brace -> this is valid + // '/test{1,}/', // Invalid quantifier -> this is valid + '/[z-a]/', // Invalid character range + 'invalid', // No delimiters + '', // Empty string + '/(?P<123>test)/', // Invalid named group name + ]; + + foreach ($invalidPatterns as $pattern) { + $this->assertFalse( + Strings::isValidRegex($pattern), + "Pattern '{$pattern}' should be invalid" + ); + } + } + + /** + * Test getLastRegexErrorString returns correct error messages + */ + public function testGetLastRegexErrorStringReturnsCorrectMessages(): void + { + // Test with a valid regex first to ensure clean state + Strings::isValidRegex('/valid/'); + $this->assertEquals('No error', Strings::getLastRegexErrorString()); + + // Test with invalid regex to trigger an error + Strings::isValidRegex('/[/'); + $errorMessage = Strings::getLastRegexErrorString(); + + // The error message should be one of the defined messages + $this->assertContains($errorMessage, array_values(Strings::PREG_ERROR_MESSAGES)); + $this->assertNotEquals('Unknown error', $errorMessage); + } + + /** + * Test getLastRegexErrorString with unknown error + */ + public function testGetLastRegexErrorStringWithUnknownError(): void + { + // This is harder to test directly since we can't easily mock preg_last_error() + // but we can test the fallback behavior by reflection or assume it works + + // At minimum, ensure it returns a string + $result = Strings::getLastRegexErrorString(); + $this->assertIsString($result); + $this->assertNotEmpty($result); + } + + /** + * Test validateRegex with valid patterns + */ + public function testValidateRegexWithValidPatterns(): void + { + $validPatterns = [ + '/^test$/', + '/\d+/', + '/[a-z]+/i', + ]; + + foreach ($validPatterns as $pattern) { + $result = Strings::validateRegex($pattern); + + $this->assertIsArray($result); + $this->assertArrayHasKey('valid', $result); + $this->assertArrayHasKey('preg_error', $result); + $this->assertArrayHasKey('error', $result); + + $this->assertTrue($result['valid'], "Pattern '{$pattern}' should be valid"); + $this->assertEquals(PREG_NO_ERROR, $result['preg_error']); + $this->assertNull($result['error']); + } + } + + /** + * Test validateRegex with invalid patterns + */ + public function testValidateRegexWithInvalidPatterns(): void + { + $invalidPatterns = [ + '/[/', // Unmatched bracket + '/(?P/', // Unmatched parenthesis + '/test\\/', // Invalid escape at end + '/(test/', // Unmatched parenthesis + ]; + + foreach ($invalidPatterns as $pattern) { + $result = Strings::validateRegex($pattern); + + $this->assertIsArray($result); + $this->assertArrayHasKey('valid', $result); + $this->assertArrayHasKey('preg_error', $result); + $this->assertArrayHasKey('error', $result); + $this->assertArrayHasKey('pcre_error', $result); + + $this->assertFalse($result['valid'], "Pattern '{$pattern}' should be invalid"); + $this->assertNotEquals(PREG_NO_ERROR, $result['preg_error']); + $this->assertIsString($result['error']); + $this->assertNotNull($result['error']); + $this->assertNotEmpty($result['error']); + + // Verify error message is from our defined messages or 'Unknown error' + $this->assertTrue( + in_array($result['error'], array_values(Strings::PREG_ERROR_MESSAGES)) || + $result['error'] === 'Unknown error' + ); + } + } + + /** + * Test validateRegex array structure + */ + public function testValidateRegexArrayStructure(): void + { + $result = Strings::validateRegex('/test/'); + + // Test array structure for valid regex + $this->assertIsArray($result); + $this->assertCount(4, $result); + $this->assertArrayHasKey('valid', $result); + $this->assertArrayHasKey('preg_error', $result); + $this->assertArrayHasKey('error', $result); + + $result = Strings::validateRegex('/[/'); + + // Test array structure for invalid regex + $this->assertIsArray($result); + $this->assertCount(4, $result); + $this->assertArrayHasKey('valid', $result); + $this->assertArrayHasKey('preg_error', $result); + $this->assertArrayHasKey('error', $result); + $this->assertArrayHasKey('pcre_error', $result); + } + + /** + * Test that methods handle edge cases properly + */ + public function testEdgeCases(): void + { + // Empty string + $this->assertFalse(Strings::isValidRegex('')); + + $result = Strings::validateRegex(''); + $this->assertFalse($result['valid']); + + // Very long pattern + $longPattern = '/' . str_repeat('a', 1000) . '/'; + $this->assertTrue(Strings::isValidRegex($longPattern)); + + // Unicode patterns + $this->assertTrue(Strings::isValidRegex('/\p{L}+/u')); + $this->assertTrue(Strings::isValidRegex('/[α-ω]+/u')); + } + + /** + * Test PREG_ERROR_MESSAGES constant accessibility + */ + public function testPregErrorMessagesConstant(): void + { + $this->assertIsArray(Strings::PREG_ERROR_MESSAGES); + $this->assertNotEmpty(Strings::PREG_ERROR_MESSAGES); + + // Check that all expected PREG constants are defined + $expectedKeys = [ + PREG_NO_ERROR, + PREG_INTERNAL_ERROR, + PREG_BACKTRACK_LIMIT_ERROR, + PREG_RECURSION_LIMIT_ERROR, + PREG_BAD_UTF8_ERROR, + PREG_BAD_UTF8_OFFSET_ERROR, + PREG_JIT_STACKLIMIT_ERROR, + ]; + + foreach ($expectedKeys as $key) { + $this->assertArrayHasKey($key, Strings::PREG_ERROR_MESSAGES); + $this->assertIsString(Strings::PREG_ERROR_MESSAGES[$key]); + $this->assertNotEmpty(Strings::PREG_ERROR_MESSAGES[$key]); + } + } + + /** + * Test error state isolation between method calls + */ + public function testErrorStateIsolation(): void + { + // Start with invalid regex + Strings::isValidRegex('/[/'); + $firstError = Strings::getLastRegexErrorString(); + $this->assertNotEquals('No error', $firstError); + + // Use valid regex + Strings::isValidRegex('/valid/'); + $secondError = Strings::getLastRegexErrorString(); + $this->assertEquals('No error', $secondError); + + // Verify validateRegex clears previous errors + $result = Strings::validateRegex('/valid/'); + $this->assertTrue($result['valid']); + $this->assertEquals(PREG_NO_ERROR, $result['preg_error']); + } + + /** + * Test various regex delimiters + */ + public function testDifferentDelimiters(): void + { + $patterns = [ + '/test/', // forward slash + '#test#', // hash + '~test~', // tilde + '|test|', // pipe + '@test@', // at symbol + '!test!', // exclamation + '%test%', // percent + ]; + + foreach ($patterns as $pattern) { + $this->assertTrue( + Strings::isValidRegex($pattern), + "Pattern with delimiter '{$pattern}' should be valid" + ); + } + } +} + +// __END__ diff --git a/4dev/tests/Convert/CoreLibsConvertStringsTest.php b/4dev/tests/Convert/CoreLibsConvertStringsTest.php index eb34d4b2..3233ce07 100644 --- a/4dev/tests/Convert/CoreLibsConvertStringsTest.php +++ b/4dev/tests/Convert/CoreLibsConvertStringsTest.php @@ -632,15 +632,33 @@ final class CoreLibsConvertStringsTest extends TestCase return [ 'valid regex' => [ '/^[A-z]$/', - true + true, + [ + 'valid' => true, + 'preg_error' => 0, + 'error' => null, + 'pcre_error' => null + ], ], 'invalid regex A' => [ '/^[A-z]$', - false + false, + [ + 'valid' => false, + 'preg_error' => 1, + 'error' => 'Internal PCRE error', + 'pcre_error' => 'Internal error' + ], ], 'invalid regex B' => [ '/^[A-z$', - false + false, + [ + 'valid' => false, + 'preg_error' => 1, + 'error' => 'Internal PCRE error', + 'pcre_error' => 'Internal error' + ], ], ]; } @@ -656,11 +674,23 @@ final class CoreLibsConvertStringsTest extends TestCase * @param bool $expected * @return void */ - public function testIsValidRegexSimple(string $input, bool $expected): void + public function testIsValidRegexSimple(string $input, bool $expected, array $expected_extended): void { $this->assertEquals( $expected, - \CoreLibs\Convert\Strings::isValidRegexSimple($input) + \CoreLibs\Convert\Strings::isValidRegex($input), + 'Regex is not valid' + ); + $this->assertEquals( + $expected_extended, + \CoreLibs\Convert\Strings::validateRegex($input), + 'Validation of regex failed' + ); + $this->assertEquals( + // for true null is set, so we get here No Error + $expected_extended['error'] ?? \CoreLibs\Convert\Strings::PREG_ERROR_MESSAGES[0], + \CoreLibs\Convert\Strings::getLastRegexErrorString(), + 'Cannot match last preg error string' ); } } diff --git a/www/admin/class_test.strings.php b/www/admin/class_test.strings.php index f49ea6ad..95e4457a 100644 --- a/www/admin/class_test.strings.php +++ b/www/admin/class_test.strings.php @@ -128,11 +128,17 @@ print "Unique: " . Strings::removeDuplicates($input_string) . "
"; print "Unique: " . Strings::removeDuplicates(strtolower($input_string)) . "
"; $regex_string = "/^[A-z]$/"; -print "Regex valid: " . $regex_string . ": " - . DgS::prBl(Strings::isValidRegexSimple($regex_string)) . "
"; +print "Regex is: " . $regex_string . ": " . DgS::prBl(Strings::isValidRegex($regex_string)) . "
"; +$regex_string = "'//test{//'"; +print "Regex is: " . $regex_string . ": " . DgS::prBl(Strings::isValidRegex($regex_string)) . "
"; +print "Regex is: " . $regex_string . ": " . DgS::printAr(Strings::validateRegex($regex_string)) . "
"; $regex_string = "/^[A-z"; -print "Regex valid: " . $regex_string . ": " - . DgS::prBl(Strings::isValidRegexSimple($regex_string)) . "
"; +print "Regex is: " . $regex_string . ": " . DgS::prBl(Strings::isValidRegex($regex_string)) . "
"; +print "[A] LAST PREGE ERROR: " . preg_last_error() . " -> " + . (Strings::PREG_ERROR_MESSAGES[preg_last_error()] ?? '-') . "
"; +$preg_error = Strings::isValidRegex($regex_string); +print "[B] LAST PREGE ERROR: " . preg_last_error() . " -> " + . Strings::getLastRegexErrorString() . " -> " . preg_last_error_msg() . "
"; print ""; diff --git a/www/lib/CoreLibs/Convert/Strings.php b/www/lib/CoreLibs/Convert/Strings.php index 9a4905ae..ae87c4d2 100644 --- a/www/lib/CoreLibs/Convert/Strings.php +++ b/www/lib/CoreLibs/Convert/Strings.php @@ -12,6 +12,16 @@ use CoreLibs\Combined\ArrayHandler; class Strings { + /** @var array all the preg error messages */ + public const array PREG_ERROR_MESSAGES = [ + PREG_NO_ERROR => 'No error', + PREG_INTERNAL_ERROR => 'Internal PCRE error', + PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit exhausted', + PREG_RECURSION_LIMIT_ERROR => 'Recursion limit exhausted', + PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data', + PREG_BAD_UTF8_OFFSET_ERROR => 'Bad UTF-8 offset', + PREG_JIT_STACKLIMIT_ERROR => 'JIT stack limit exhausted' + ]; /** * return the number of elements in the split list * 0 if nothing / invalid split @@ -259,8 +269,9 @@ class Strings * @param string $pattern Any regex string * @return bool False on invalid regex */ - public static function isValidRegexSimple(string $pattern): bool + public static function isValidRegex(string $pattern): bool { + preg_last_error(); try { $var = ''; @preg_match($pattern, $var); @@ -269,6 +280,41 @@ class Strings return false; } } + + /** + * Returns the last preg error messages as string + * all messages are defined in PREG_ERROR_MESSAGES + * + * @return string + */ + public static function getLastRegexErrorString(): string + { + return self::PREG_ERROR_MESSAGES[preg_last_error()] ?? 'Unknown error'; + } + + /** + * check if a regex is invalid, returns array with flag and error string + * + * @param string $pattern + * @return array{valid:bool,preg_error:0,error:null|string,pcre_error:null|string} + */ + public static function validateRegex(string $pattern): array + { + // Clear any previous PCRE errors + preg_last_error(); + $var = ''; + if (@preg_match($pattern, $var) === false) { + $error = preg_last_error(); + return [ + 'valid' => false, + 'preg_error' => $error, + 'error' => self::PREG_ERROR_MESSAGES[$error] ?? 'Unknown error', + 'pcre_error' => preg_last_error_msg(), + ]; + } + + return ['valid' => true, 'preg_error' => PREG_NO_ERROR, 'error' => null, 'pcre_error' => null]; + } } // __END__