Skip to content

Commit 33def41

Browse files
feature #439 Add polyfill for ini_parse_quantity based on the C code in PHP (fisharebest)
This PR was squashed before being merged into the 1.x branch. Discussion ---------- Add polyfill for ini_parse_quantity based on the C code in PHP See #411 for a previous attempt to polyfill this function. The `ini_parse_quantity()` is difficult to polyfill as the error handling is non-trivial, and several invalid inputs are treated as valid. This polyfill is a simple/direct implementation of the C code from PHP. As such it uses `goto`s and pointer-based string access. I am fully aware that this would fail most code reviews :-) However it works and I hope it will be useful until a cleaner implementation can be provided. The unit tests provide 100% code coverage. I have also included (but commented out) a brute-force test which takes many hours to run. I found it was very helpful in finding edge cases. It may be useful for anyone who tries to refactor the code. The two overflow tests will probably fail on a 32bit build of PHP, however the code itself should work. I just haven't been able to work out the expected values. Commits ------- d07580f Add polyfill for ini_parse_quantity based on the C code in PHP
2 parents 2776a61 + d07580f commit 33def41

File tree

6 files changed

+629
-0
lines changed

6 files changed

+629
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* Polyfill `odbc_connection_string_is_quoted()`
77
* Polyfill `odbc_connection_string_should_quote()`
88
* Polyfill `odbc_connection_string_quote()`
9+
* Polyfill `ini_parse_quantity()`
910
* Polyfill `mb_str_pad()`
1011
* Polyfill `#[\Override]` attribute
1112
* Use full case folding when using `MB_CASE_FOLD`

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Polyfills are provided for:
7272
- the `odbc_connection_string_is_quoted` function introduced in PHP 8.2;
7373
- the `odbc_connection_string_should_quote` function introduced in PHP 8.2;
7474
- the `odbc_connection_string_quote` function introduced in PHP 8.2;
75+
- the `ini_parse_quantity` function introduced in PHP 8.2;
7576
- the `json_validate` function introduced in PHP 8.3;
7677
- the `Override` attribute introduced in PHP 8.3;
7778
- the `mb_str_pad` function introduced in PHP 8.3;

src/Php82/Php82.php

+294
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
/**
1515
* @author Alexander M. Turek <[email protected]>
16+
* @author Greg Roach <[email protected]>
1617
*
1718
* @internal
1819
*/
@@ -71,4 +72,297 @@ public static function odbc_connection_string_quote(string $str): string
7172
{
7273
return '{'.str_replace('}', '}}', $str).'}';
7374
}
75+
76+
/**
77+
* Implementation closely based on the original C code - including the GOTOs
78+
* and pointer-style string access.
79+
*
80+
* @see https://github.com/php/php-src/blob/master/Zend/zend_ini.c
81+
*/
82+
public static function ini_parse_quantity(string $value): int
83+
{
84+
// Avoid dependency on ctype_space()
85+
$ctype_space = " \t\v\r\n\f";
86+
87+
$str = 0;
88+
$str_end = \strlen($value);
89+
$digits = $str;
90+
$overflow = false;
91+
92+
/* Ignore leading whitespace, but keep it for error messages. */
93+
while ($digits < $str_end && false !== strpos($ctype_space, $value[$digits])) {
94+
++$digits;
95+
}
96+
97+
/* Ignore trailing whitespace, but keep it for error messages. */
98+
while ($digits < $str_end && false !== strpos($ctype_space, $value[$str_end - 1])) {
99+
--$str_end;
100+
}
101+
102+
if ($digits === $str_end) {
103+
return 0;
104+
}
105+
106+
$is_negative = false;
107+
108+
if ('+' === $value[$digits]) {
109+
++$digits;
110+
} elseif ('-' === $value[$digits]) {
111+
$is_negative = true;
112+
++$digits;
113+
}
114+
115+
if ($value[$digits] < '0' || $value[$digits] > 9) {
116+
$message = sprintf(
117+
'Invalid quantity "%s": no valid leading digits, interpreting as "0" for backwards compatibility',
118+
self::escapeString($value)
119+
);
120+
121+
trigger_error($message, \E_USER_WARNING);
122+
123+
return 0;
124+
}
125+
126+
$base = 10;
127+
$allowed_digits = '0123456789';
128+
129+
if ('0' === $value[$digits] && ($digits + 1 === $str_end || false === strpos($allowed_digits, $value[$digits + 1]))) {
130+
if ($digits + 1 === $str_end) {
131+
return 0;
132+
}
133+
134+
switch ($value[$digits + 1]) {
135+
case 'g':
136+
case 'G':
137+
case 'm':
138+
case 'M':
139+
case 'k':
140+
case 'K':
141+
goto evaluation;
142+
case 'x':
143+
case 'X':
144+
$base = 16;
145+
$allowed_digits = '0123456789abcdefABCDEF';
146+
break;
147+
case 'o':
148+
case 'O':
149+
$base = 8;
150+
$allowed_digits = '01234567';
151+
break;
152+
case 'b':
153+
case 'B':
154+
$base = 2;
155+
$allowed_digits = '01';
156+
break;
157+
default:
158+
$message = sprintf(
159+
'Invalid prefix "0%s", interpreting as "0" for backwards compatibility',
160+
$value[$digits + 1]
161+
);
162+
trigger_error($message, \E_USER_WARNING);
163+
164+
return 0;
165+
}
166+
167+
$digits += 2;
168+
if ($digits === $str_end) {
169+
$message = sprintf(
170+
'Invalid quantity "%s": no digits after base prefix, interpreting as "0" for backwards compatibility',
171+
self::escapeString($value)
172+
);
173+
trigger_error($message, \E_USER_WARNING);
174+
175+
return 0;
176+
}
177+
}
178+
179+
evaluation:
180+
181+
if (10 === $base && '0' === $value[$digits]) {
182+
$base = 8;
183+
$allowed_digits = '01234567';
184+
}
185+
186+
while ($digits < $str_end && ' ' === $value[$digits]) {
187+
++$digits;
188+
}
189+
190+
if ($digits < $str_end && '+' === $value[$digits]) {
191+
++$digits;
192+
} elseif ($digits < $str_end && '-' === $value[$digits]) {
193+
$is_negative = true;
194+
$overflow = true;
195+
++$digits;
196+
}
197+
198+
$digits_end = $digits;
199+
200+
// The native function treats 0x0x123 the same as 0x123. This is a bug which we must replicate.
201+
if (
202+
16 === $base
203+
&& $digits_end + 2 < $str_end
204+
&& '0x' === substr($value, $digits_end, 2)
205+
&& false !== strpos($allowed_digits, $value[$digits_end + 2])
206+
) {
207+
$digits_end += 2;
208+
}
209+
210+
while ($digits_end < $str_end && false !== strpos($allowed_digits, $value[$digits_end])) {
211+
++$digits_end;
212+
}
213+
214+
$retval = base_convert(substr($value, $digits, $digits_end - $digits), $base, 10);
215+
216+
if ($is_negative && '0' === $retval) {
217+
$is_negative = false;
218+
$overflow = false;
219+
}
220+
221+
// Check for overflow - remember that -PHP_INT_MIN = 1 + PHP_INT_MAX
222+
if ($is_negative) {
223+
$signed_max = strtr((string) \PHP_INT_MIN, ['-' => '']);
224+
} else {
225+
$signed_max = (string) \PHP_INT_MAX;
226+
}
227+
228+
$max_length = max(\strlen($retval), \strlen($signed_max));
229+
230+
$tmp1 = str_pad($retval, $max_length, '0', \STR_PAD_LEFT);
231+
$tmp2 = str_pad($signed_max, $max_length, '0', \STR_PAD_LEFT);
232+
233+
if ($tmp1 > $tmp2) {
234+
$retval = -1;
235+
$overflow = true;
236+
} elseif ($is_negative) {
237+
$retval = '-'.$retval;
238+
}
239+
240+
$retval = (int) $retval;
241+
242+
if ($digits_end === $digits) {
243+
$message = sprintf(
244+
'Invalid quantity "%s": no valid leading digits, interpreting as "0" for backwards compatibility',
245+
self::escapeString($value)
246+
);
247+
trigger_error($message, \E_USER_WARNING);
248+
249+
return 0;
250+
}
251+
252+
/* Allow for whitespace between integer portion and any suffix character */
253+
while ($digits_end < $str_end && false !== strpos($ctype_space, $value[$digits_end])) {
254+
++$digits_end;
255+
}
256+
257+
/* No exponent suffix. */
258+
if ($digits_end === $str_end) {
259+
goto end;
260+
}
261+
262+
switch ($value[$str_end - 1]) {
263+
case 'g':
264+
case 'G':
265+
$shift = 30;
266+
break;
267+
case 'm':
268+
case 'M':
269+
$shift = 20;
270+
break;
271+
case 'k':
272+
case 'K':
273+
$shift = 10;
274+
break;
275+
default:
276+
/* Unknown suffix */
277+
$invalid = self::escapeString($value);
278+
$interpreted = self::escapeString(substr($value, $str, $digits_end - $str));
279+
$chr = self::escapeString($value[$str_end - 1]);
280+
281+
$message = sprintf(
282+
'Invalid quantity "%s": unknown multiplier "%s", interpreting as "%s" for backwards compatibility',
283+
$invalid,
284+
$chr,
285+
$interpreted
286+
);
287+
288+
trigger_error($message, \E_USER_WARNING);
289+
290+
return $retval;
291+
}
292+
293+
$factor = 1 << $shift;
294+
295+
if (!$overflow) {
296+
if ($retval > 0) {
297+
$overflow = $retval > \PHP_INT_MAX / $factor;
298+
} else {
299+
$overflow = $retval < \PHP_INT_MIN / $factor;
300+
}
301+
}
302+
303+
if (\is_float($retval * $factor)) {
304+
$overflow = true;
305+
$retval <<= $shift;
306+
} else {
307+
$retval *= $factor;
308+
}
309+
310+
if ($digits_end !== $str_end - 1) {
311+
/* More than one character in suffix */
312+
$message = sprintf(
313+
'Invalid quantity "%s", interpreting as "%s%s" for backwards compatibility',
314+
self::escapeString($value),
315+
self::escapeString(substr($value, $str, $digits_end - $str)),
316+
self::escapeString($value[$str_end - 1])
317+
);
318+
trigger_error($message, \E_USER_WARNING);
319+
320+
return $retval;
321+
}
322+
323+
end:
324+
325+
if ($overflow) {
326+
/* Not specifying the resulting value here because the caller may make
327+
* additional conversions. Not specifying the allowed range
328+
* because the caller may do narrower range checks. */
329+
$message = sprintf(
330+
'Invalid quantity "%s": value is out of range, using overflow result for backwards compatibility',
331+
self::escapeString($value)
332+
);
333+
trigger_error($message, \E_USER_WARNING);
334+
}
335+
336+
return $retval;
337+
}
338+
339+
/**
340+
* Escape the string to avoid null bytes and to make non-printable chars visible.
341+
*/
342+
private static function escapeString(string $string): string
343+
{
344+
$escaped = '';
345+
346+
for ($n = 0, $len = \strlen($string); $n < $len; ++$n) {
347+
$c = \ord($string[$n]);
348+
349+
if ($c < 32 || '\\' === $string[$n] || $c > 126) {
350+
switch ($string[$n]) {
351+
case "\n": $escaped .= '\\n'; break;
352+
case "\r": $escaped .= '\\r'; break;
353+
case "\t": $escaped .= '\\t'; break;
354+
case "\f": $escaped .= '\\f'; break;
355+
case "\v": $escaped .= '\\v'; break;
356+
case '\\': $escaped .= '\\\\'; break;
357+
case "\x1B": $escaped .= '\\e'; break;
358+
default:
359+
$escaped .= '\\x'.strtoupper(sprintf('%02x', $c));
360+
}
361+
} else {
362+
$escaped .= $string[$n];
363+
}
364+
}
365+
366+
return $escaped;
367+
}
74368
}

src/Php82/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This component provides features added to PHP 8.2 core:
1212
- [`odbc_connection_string_is_quoted()`](https://php.net/odbc_connection_string_is_quoted)
1313
- [`odbc_connection_string_should_quote()`](https://php.net/odbc_connection_string_should_quote)
1414
- [`odbc_connection_string_quote()`](https://php.net/odbc_connection_string_quote)
15+
- [`ini_parse_quantity()`](https://php.net/ini_parse_quantity)
1516

1617
More information can be found in the
1718
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).

src/Php82/bootstrap.php

+4
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@ function odbc_connection_string_should_quote(string $str): bool { return p\Php82
3030
if (!function_exists('odbc_connection_string_quote')) {
3131
function odbc_connection_string_quote(string $str): string { return p\Php82::odbc_connection_string_quote($str); }
3232
}
33+
34+
if (!function_exists('ini_parse_quantity')) {
35+
function ini_parse_quantity(string $shorthand): int { return p\Php82::ini_parse_quantity($shorthand); }
36+
}

0 commit comments

Comments
 (0)