diff --git a/ext/standard/VmSscanf.php b/ext/standard/VmSscanf.php index b25cf606e1d..229471c392b 100644 --- a/ext/standard/VmSscanf.php +++ b/ext/standard/VmSscanf.php @@ -60,6 +60,21 @@ public static function parseWithConsumed(string $input, string $format, array $o if ($fpos >= $fmtLen) { return [$assigned, $inPos]; } + if ('[' === $format[$fpos]) { + [$matcher, $fpos] = self::parseScansetMatcher($format, $fpos, $fmtLen); + if ($outIdx >= \count($outVars)) { + return [$assigned, $inPos]; + } + [$str, $consumed] = self::scanScansetMatch($input, $inPos, $inLen, $matcher, $width); + if (null === $str) { + return [$assigned, $inPos]; + } + self::assignString($outVars[$outIdx], $str); + $inPos += $consumed; + ++$outIdx; + ++$assigned; + continue; + } $spec = $format[$fpos]; if ('%' === $spec) { if ($inPos >= $inLen || $input[$inPos] !== '%') { @@ -240,6 +255,11 @@ public static function countConversionSpecs(string $format): int if ($fpos >= $len) { break; } + if ('[' === $format[$fpos]) { + [, $fpos] = self::parseScansetMatcher($format, $fpos, $len); + ++$count; + continue; + } $spec = $format[$fpos]; if ('%' !== $spec) { ++$count; @@ -265,6 +285,10 @@ public static function validateConversionCharacters(string $format): void if ($fpos >= $len) { break; } + if ('[' === $format[$fpos]) { + self::parseScansetMatcher($format, $fpos, $len); + continue; + } $spec = $format[$fpos]; if ('%' === $spec) { continue; @@ -275,6 +299,95 @@ public static function validateConversionCharacters(string $format): void } } + /** + * %[scanset] — php-src ext/standard/formatted_io.c scan_set_conversion(). + * + * @return array{0: callable(string): bool, 1: int} + */ + private static function parseScansetMatcher(string $format, int $fpos, int $fmtLen): array + { + if ($fpos >= $fmtLen || '[' !== $format[$fpos]) { + throw new \ValueError('Bad scan conversion character "["'); + } + ++$fpos; + $negated = false; + if ($fpos < $fmtLen && '^' === $format[$fpos]) { + $negated = true; + ++$fpos; + } + /** @var array $chars */ + $chars = []; + if ($fpos >= $fmtLen) { + throw new \ValueError('Unmatched [ in format string'); + } + if (']' === $format[$fpos]) { + throw new \ValueError('Unmatched [ in format string'); + } + while ($fpos < $fmtLen && ']' !== $format[$fpos]) { + $ch = $format[$fpos]; + if ( + $fpos + 2 < $fmtLen + && '-' === $format[$fpos + 1] + && ']' !== $format[$fpos + 2] + ) { + $lo = $ch; + $hi = $format[$fpos + 2]; + if (ord($lo) <= ord($hi)) { + for ($c = ord($lo); $c <= ord($hi); ++$c) { + $chars[chr($c)] = true; + } + $fpos += 3; + continue; + } + } + $chars[$ch] = true; + ++$fpos; + } + if ($fpos >= $fmtLen || ']' !== $format[$fpos]) { + throw new \ValueError('Unmatched [ in format string'); + } + ++$fpos; + $matcher = static function (string $ch) use ($chars, $negated): bool { + $in = isset($chars[$ch]); + + return $negated ? !$in : $in; + }; + + return [$matcher, $fpos]; + } + + /** + * @param callable(string): bool $matcher + * + * @return array{0: ?string, 1: int} + */ + private static function scanScansetMatch( + string $input, + int $pos, + int $len, + callable $matcher, + ?int $maxWidth = null + ): array { + $orig = $pos; + $start = $pos; + $read = 0; + while ($pos < $len) { + if (null !== $maxWidth && $read >= $maxWidth) { + break; + } + if (!$matcher($input[$pos])) { + break; + } + ++$pos; + ++$read; + } + if ($start === $pos) { + return [null, 0]; + } + + return [substr($input, $start, $pos - $start), $pos - $orig]; + } + /** * Optional decimal field width after '%' (php-src ext/standard/formatted_io.c). * diff --git a/test/compliance/cases/stdlib/sscanf_scanset.phpt b/test/compliance/cases/stdlib/sscanf_scanset.phpt new file mode 100644 index 00000000000..bfc8da7fd6d --- /dev/null +++ b/test/compliance/cases/stdlib/sscanf_scanset.phpt @@ -0,0 +1,22 @@ +--TEST-- +stdlib sscanf() — %[scanset] conversion (#11281, ext/standard/formatted_io.c) +--FILE-- + 'abc', +) +array ( + 0 => 'abc', +) +array ( + 0 => 'xyz', +) diff --git a/test/repro/maintainer_gap_sscanf_scanset.php b/test/repro/maintainer_gap_sscanf_scanset.php new file mode 100644 index 00000000000..c3a94126de2 --- /dev/null +++ b/test/repro/maintainer_gap_sscanf_scanset.php @@ -0,0 +1,10 @@ +