diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f450026..71a10e1d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: uses: shivammathur/setup-php@v2 with: coverage: "none" - extensions: "apcu, intl, mbstring, uuid" + extensions: "apcu, intl, mbstring, odbc, uuid" ini-values: "memory_limit=-1, session.gc_probability=0, apc.enable_cli=1" php-version: "${{ matrix.php }}" tools: "composer:v2" diff --git a/src/Php82/Php82.php b/src/Php82/Php82.php new file mode 100644 index 00000000..8a4e7b5e --- /dev/null +++ b/src/Php82/Php82.php @@ -0,0 +1,72 @@ + + * + * @internal + */ +class Php82 +{ + /** + * Determines if a string matches the ODBC quoting rules. + * + * A valid quoted string begins with a '{', ends with a '}', and has no '}' + * inside of the string that aren't repeated (as to be escaped). + * + * These rules are what .NET also follows. + * + * @see https://github.com/php/php-src/blob/838f6bffff6363a204a2597cbfbaad1d7ee3f2b6/main/php_odbc_utils.c#L31-L57 + */ + public static function odbc_connection_string_is_quoted(string $str): bool + { + if ($str === '' || $str[0] !== '{') { + return false; + } + + /* Check for } that aren't doubled up or at the end of the string */ + $length = strlen($str); + for ($i = 0; $i < $length; $i++) { + if ($str[$i] !== '}') { + continue; + } + + if ($i + 2 < $length && $str[$i + 1] === '}') { + /* Skip over so we don't count it again */ + $i++; + + continue; + } + + + /* If not at the end, not quoted */ + return $i + 1 === $length; + } + + return true; + } + + /** + * Determines if a value for a connection string should be quoted. + * + * The ODBC specification mentions: + * "Because of connection string and initialization file grammar, keywords and + * attribute values that contain the characters []{}(),;?*=!@ not enclosed + * with braces should be avoided." + * + * Note that it assumes that the string is *not* already quoted. You should + * check beforehand. + * + * @see https://github.com/php/php-src/blob/838f6bffff6363a204a2597cbfbaad1d7ee3f2b6/main/php_odbc_utils.c#L59-L73 + */ + public static function odbc_connection_string_should_quote(string $str): bool + { + return strpbrk($str, '[]{}(),;?*=!@') !== false; + } + + public static function odbc_connection_string_quote(string $str): string + { + return '{' . str_replace('}', '}}', $str) . '}'; + } +} diff --git a/src/Php82/README.md b/src/Php82/README.md index 4a4d8817..9038382a 100644 --- a/src/Php82/README.md +++ b/src/Php82/README.md @@ -9,6 +9,9 @@ This component provides features added to PHP 8.2 core: - [`Random\Engine`](https://wiki.php.net/rfc/rng_extension) - [`Random\Engine\CryptoSafeEngine`](https://wiki.php.net/rfc/rng_extension) - [`Random\Engine\Secure`](https://wiki.php.net/rfc/rng_extension) (check [arokettu/random-polyfill](https://packagist.org/packages/arokettu/random-polyfill) for more engines) +- [`odbc_connection_string_is_quoted()`](https://php.net/odbc_connection_string_is_quoted) +- [`odbc_connection_string_should_quote()`](https://php.net/odbc_connection_string_should_quote) +- [`odbc_connection_string_quote()`](https://php.net/odbc_connection_string_quote) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). diff --git a/src/Php82/bootstrap.php b/src/Php82/bootstrap.php index 1ebfdba2..ad382c20 100644 --- a/src/Php82/bootstrap.php +++ b/src/Php82/bootstrap.php @@ -9,6 +9,20 @@ * file that was distributed with this source code. */ +use Symfony\Polyfill\Php82 as p; + if (\PHP_VERSION_ID >= 80200) { return; } + +if (extension_loaded('odbc') && !function_exists('odbc_connection_string_is_quoted')) { + function odbc_connection_string_is_quoted(string $str): bool { return p\Php82::odbc_connection_string_is_quoted($str); } +} + +if (extension_loaded('odbc') && !function_exists('odbc_connection_string_should_quote')) { + function odbc_connection_string_should_quote(string $str): bool { return p\Php82::odbc_connection_string_should_quote($str); } +} + +if (extension_loaded('odbc') && !function_exists('odbc_connection_string_quote')) { + function odbc_connection_string_quote(string $str): string { return p\Php82::odbc_connection_string_quote($str); } +} diff --git a/tests/Php82/Php82Test.php b/tests/Php82/Php82Test.php new file mode 100644 index 00000000..85add183 --- /dev/null +++ b/tests/Php82/Php82Test.php @@ -0,0 +1,75 @@ + + */ + public static function provideConnectionStringValuesFromUpstream(): \Generator + { + // 1. No, it's not quoted. + // 2. Yes, it should be quoted because of the special character in the middle. + yield 'with_end_curly1' => ['foo}bar', false, true, '{foo}}bar}']; + + // 1. No, the unescaped special character in the middle breaks what would be quoted. + // 2. Yes, it should be quoted because of the special character in the middle. + // Note that should_quote doesn't care about if the string is already quoted. + // That's why you should check if it is quoted first. + yield 'with_end_curly2' => ['{foo}bar}', false, true, '{{foo}}bar}}}']; + + // 1. Yes, the special characters are escaped, so it's quoted. + // 2. See $with_end_curly2; should_quote doesn't care about if the string is already quoted. + yield 'with_end_curly3' => ['{foo}}bar}', true, true, '{{foo}}}}bar}}}']; + + // 1. No, it's not quoted. + // 2. It doesn't need to be quoted because of no s + yield 'with_no_end_curly1' => ['foobar', false, false, '{foobar}']; + + // 1. Yes, it is quoted and any characters are properly escaped. + // 2. See $with_end_curly2. + yield 'with_no_end_curly2' => ['{foobar}', true, true, '{{foobar}}}']; + } + + /** + * @return \Generator + */ + public static function provideMoreConnectionStringValues(): \Generator + { + yield 'double curly at the end' => ['foo}}', false, true, '{foo}}}}}']; + } +}