* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php82; /** * @author Alexander M. Turek * @author Greg Roach * * @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) - 1; for ($i = 0; $i < $length; ++$i) { if ('}' !== $str[$i]) { continue; } if ('}' !== $str[++$i]) { return $i === $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 false !== strpbrk($str, '[]{}(),;?*=!@'); } public static function odbc_connection_string_quote(string $str): string { return '{'.str_replace('}', '}}', $str).'}'; } /** * Implementation closely based on the original C code - including the GOTOs * and pointer-style string access. * * @see https://github.com/php/php-src/blob/master/Zend/zend_ini.c */ public static function ini_parse_quantity(string $value): int { // Avoid dependency on ctype_space() $ctype_space = " \t\v\r\n\f"; $str = 0; $str_end = \strlen($value); $digits = $str; $overflow = false; /* Ignore leading whitespace, but keep it for error messages. */ while ($digits < $str_end && false !== strpos($ctype_space, $value[$digits])) { ++$digits; } /* Ignore trailing whitespace, but keep it for error messages. */ while ($digits < $str_end && false !== strpos($ctype_space, $value[$str_end - 1])) { --$str_end; } if ($digits === $str_end) { return 0; } $is_negative = false; if ('+' === $value[$digits]) { ++$digits; } elseif ('-' === $value[$digits]) { $is_negative = true; ++$digits; } if ($value[$digits] < '0' || $value[$digits] > 9) { $message = sprintf( 'Invalid quantity "%s": no valid leading digits, interpreting as "0" for backwards compatibility', self::escapeString($value) ); trigger_error($message, \E_USER_WARNING); return 0; } $base = 10; $allowed_digits = '0123456789'; if ('0' === $value[$digits] && ($digits + 1 === $str_end || false === strpos($allowed_digits, $value[$digits + 1]))) { if ($digits + 1 === $str_end) { return 0; } switch ($value[$digits + 1]) { case 'g': case 'G': case 'm': case 'M': case 'k': case 'K': goto evaluation; case 'x': case 'X': $base = 16; $allowed_digits = '0123456789abcdefABCDEF'; break; case 'o': case 'O': $base = 8; $allowed_digits = '01234567'; break; case 'b': case 'B': $base = 2; $allowed_digits = '01'; break; default: $message = sprintf( 'Invalid prefix "0%s", interpreting as "0" for backwards compatibility', $value[$digits + 1] ); trigger_error($message, \E_USER_WARNING); return 0; } $digits += 2; if ($digits === $str_end) { $message = sprintf( 'Invalid quantity "%s": no digits after base prefix, interpreting as "0" for backwards compatibility', self::escapeString($value) ); trigger_error($message, \E_USER_WARNING); return 0; } } evaluation: if (10 === $base && '0' === $value[$digits]) { $base = 8; $allowed_digits = '01234567'; } while ($digits < $str_end && ' ' === $value[$digits]) { ++$digits; } if ($digits < $str_end && '+' === $value[$digits]) { ++$digits; } elseif ($digits < $str_end && '-' === $value[$digits]) { $is_negative = true; $overflow = true; ++$digits; } $digits_end = $digits; // The native function treats 0x0x123 the same as 0x123. This is a bug which we must replicate. if ( 16 === $base && $digits_end + 2 < $str_end && '0x' === substr($value, $digits_end, 2) && false !== strpos($allowed_digits, $value[$digits_end + 2]) ) { $digits_end += 2; } while ($digits_end < $str_end && false !== strpos($allowed_digits, $value[$digits_end])) { ++$digits_end; } $retval = base_convert(substr($value, $digits, $digits_end - $digits), $base, 10); if ($is_negative && '0' === $retval) { $is_negative = false; $overflow = false; } // Check for overflow - remember that -PHP_INT_MIN = 1 + PHP_INT_MAX if ($is_negative) { $signed_max = strtr((string) \PHP_INT_MIN, ['-' => '']); } else { $signed_max = (string) \PHP_INT_MAX; } $max_length = max(\strlen($retval), \strlen($signed_max)); $tmp1 = str_pad($retval, $max_length, '0', \STR_PAD_LEFT); $tmp2 = str_pad($signed_max, $max_length, '0', \STR_PAD_LEFT); if ($tmp1 > $tmp2) { $retval = -1; $overflow = true; } elseif ($is_negative) { $retval = '-'.$retval; } $retval = (int) $retval; if ($digits_end === $digits) { $message = sprintf( 'Invalid quantity "%s": no valid leading digits, interpreting as "0" for backwards compatibility', self::escapeString($value) ); trigger_error($message, \E_USER_WARNING); return 0; } /* Allow for whitespace between integer portion and any suffix character */ while ($digits_end < $str_end && false !== strpos($ctype_space, $value[$digits_end])) { ++$digits_end; } /* No exponent suffix. */ if ($digits_end === $str_end) { goto end; } switch ($value[$str_end - 1]) { case 'g': case 'G': $shift = 30; break; case 'm': case 'M': $shift = 20; break; case 'k': case 'K': $shift = 10; break; default: /* Unknown suffix */ $invalid = self::escapeString($value); $interpreted = self::escapeString(substr($value, $str, $digits_end - $str)); $chr = self::escapeString($value[$str_end - 1]); $message = sprintf( 'Invalid quantity "%s": unknown multiplier "%s", interpreting as "%s" for backwards compatibility', $invalid, $chr, $interpreted ); trigger_error($message, \E_USER_WARNING); return $retval; } $factor = 1 << $shift; if (!$overflow) { if ($retval > 0) { $overflow = $retval > \PHP_INT_MAX / $factor; } else { $overflow = $retval < \PHP_INT_MIN / $factor; } } if (\is_float($retval * $factor)) { $overflow = true; $retval <<= $shift; } else { $retval *= $factor; } if ($digits_end !== $str_end - 1) { /* More than one character in suffix */ $message = sprintf( 'Invalid quantity "%s", interpreting as "%s%s" for backwards compatibility', self::escapeString($value), self::escapeString(substr($value, $str, $digits_end - $str)), self::escapeString($value[$str_end - 1]) ); trigger_error($message, \E_USER_WARNING); return $retval; } end: if ($overflow) { /* Not specifying the resulting value here because the caller may make * additional conversions. Not specifying the allowed range * because the caller may do narrower range checks. */ $message = sprintf( 'Invalid quantity "%s": value is out of range, using overflow result for backwards compatibility', self::escapeString($value) ); trigger_error($message, \E_USER_WARNING); } return $retval; } /** * Escape the string to avoid null bytes and to make non-printable chars visible. */ private static function escapeString(string $string): string { $escaped = ''; for ($n = 0, $len = \strlen($string); $n < $len; ++$n) { $c = \ord($string[$n]); if ($c < 32 || '\\' === $string[$n] || $c > 126) { switch ($string[$n]) { case "\n": $escaped .= '\\n'; break; case "\r": $escaped .= '\\r'; break; case "\t": $escaped .= '\\t'; break; case "\f": $escaped .= '\\f'; break; case "\v": $escaped .= '\\v'; break; case '\\': $escaped .= '\\\\'; break; case "\x1B": $escaped .= '\\e'; break; default: $escaped .= '\\x'.strtoupper(sprintf('%02x', $c)); } } else { $escaped .= $string[$n]; } } return $escaped; } }