Strings regex validation
Update regex validation and add validation class to return full information. Add helper to return last error message from defined constant
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
// This code was created by Claude Sonnet 4
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
// This code was create by Claude Sonnet 4
|
||||
// This code was created by Claude Sonnet 4
|
||||
// modification for value checks with assertEqualsCanonicalizing
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
// create by Claude Sonnet 4
|
||||
// created by Claude Sonnet 4
|
||||
|
||||
// testRecursiveSearchWithFlatResult had wrong retunr count
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
// This code was create by Claude Sonnet 4
|
||||
// This code was created by Claude Sonnet 4
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
283
4dev/tests/Convert/CoreLibsConvertStringsRegexValidateTest.php
Normal file
283
4dev/tests/Convert/CoreLibsConvertStringsRegexValidateTest.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
// This code was created by Claude Sonnet 4
|
||||
// FIX:
|
||||
// '/test{/', // Unmatched brace -> 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<name>/', // 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<name>/', // 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__
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,11 +128,17 @@ print "Unique: " . Strings::removeDuplicates($input_string) . "<br>";
|
||||
print "Unique: " . Strings::removeDuplicates(strtolower($input_string)) . "<br>";
|
||||
|
||||
$regex_string = "/^[A-z]$/";
|
||||
print "Regex valid: " . $regex_string . ": "
|
||||
. DgS::prBl(Strings::isValidRegexSimple($regex_string)) . "<br>";
|
||||
print "Regex is: " . $regex_string . ": " . DgS::prBl(Strings::isValidRegex($regex_string)) . "<br>";
|
||||
$regex_string = "'//test{//'";
|
||||
print "Regex is: " . $regex_string . ": " . DgS::prBl(Strings::isValidRegex($regex_string)) . "<br>";
|
||||
print "Regex is: " . $regex_string . ": " . DgS::printAr(Strings::validateRegex($regex_string)) . "<br>";
|
||||
$regex_string = "/^[A-z";
|
||||
print "Regex valid: " . $regex_string . ": "
|
||||
. DgS::prBl(Strings::isValidRegexSimple($regex_string)) . "<br>";
|
||||
print "Regex is: " . $regex_string . ": " . DgS::prBl(Strings::isValidRegex($regex_string)) . "<br>";
|
||||
print "[A] LAST PREGE ERROR: " . preg_last_error() . " -> "
|
||||
. (Strings::PREG_ERROR_MESSAGES[preg_last_error()] ?? '-') . "<br>";
|
||||
$preg_error = Strings::isValidRegex($regex_string);
|
||||
print "[B] LAST PREGE ERROR: " . preg_last_error() . " -> "
|
||||
. Strings::getLastRegexErrorString() . " -> " . preg_last_error_msg() . "<br>";
|
||||
|
||||
print "</body></html>";
|
||||
|
||||
|
||||
@@ -12,6 +12,16 @@ use CoreLibs\Combined\ArrayHandler;
|
||||
|
||||
class Strings
|
||||
{
|
||||
/** @var array<int,string> 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__
|
||||
|
||||
Reference in New Issue
Block a user