diff --git a/ext/standard/JitCsvArg.php b/ext/standard/JitCsvArg.php new file mode 100644 index 00000000000..b41429e7990 --- /dev/null +++ b/ext/standard/JitCsvArg.php @@ -0,0 +1,70 @@ += 3) { + self::validateArg($context, $args[2], 3, 'separator', false); + } + if ($argc >= 4) { + self::validateArg($context, $args[3], 4, 'enclosure', false); + } + if ($argc >= 5) { + self::validateArg($context, $args[4], 5, 'escape', true); + } + } + + private static function validateArg( + Context $context, + JITVariable $arg, + int $argNum, + string $paramName, + bool $allowEmpty, + ): void { + if (null !== ($arg->compileTimeString ?? null)) { + return; + } + + $message = $allowEmpty + ? \sprintf('fputcsv(): Argument #%d ($%s) must be empty or a single character', $argNum, $paramName) + : \sprintf('fputcsv(): Argument #%d ($%s) must be a single character', $argNum, $paramName); + + TypeErrorRaise::registerDeclarations($context); + TypeErrorRaise::ensureLinked($context); + $strPtr = $context->builder->load( + $context->helper->loadValue($arg) + ); + $map = $context->structFieldMap['__string__']; + $len = $context->builder->load( + $context->builder->structGep($strPtr, $map['length']) + ); + $i64 = $context->getTypeFromString('int64'); + $one = $i64->constInt(1, false); + $failBlock = BasicBlockHelper::append($context, 'fputcsv_csv_arg_fail_'.$argNum); + $okBlock = BasicBlockHelper::append($context, 'fputcsv_csv_arg_ok_'.$argNum); + if ($allowEmpty) { + $tooLong = $context->builder->icmp(Builder::INT_SGT, $len, $one); + $context->builder->branchIf($tooLong, $failBlock, $okBlock); + } else { + $notOne = $context->builder->icmp(Builder::INT_NE, $len, $one); + $context->builder->branchIf($notOne, $failBlock, $okBlock); + } + $context->builder->positionAtEnd($failBlock); + TypeErrorRaise::emitValueError($context, $message); + $context->builder->call($context->lookupFunction('abort')); + $context->builder->positionAtEnd($okBlock); + } +} diff --git a/ext/standard/VmCsvArg.php b/ext/standard/VmCsvArg.php new file mode 100644 index 00000000000..1e5fc8717b2 --- /dev/null +++ b/ext/standard/VmCsvArg.php @@ -0,0 +1,43 @@ + 1) { + throw new \ValueError(\sprintf( + 'fputcsv(): Argument #%d ($%s) must be empty or a single character', + $argNum, + $paramName + )); + } + } +} diff --git a/ext/standard/fputcsv.php b/ext/standard/fputcsv.php index 6d22c5c3967..f60c689d4e4 100644 --- a/ext/standard/fputcsv.php +++ b/ext/standard/fputcsv.php @@ -6,6 +6,8 @@ use PHPCompiler\Frame; use PHPCompiler\Func\Internal; +use PHPCompiler\JIT\BasicBlockHelper; +use PHPCompiler\JIT\Builtin\TypeErrorRaise; use PHPCompiler\JIT\Context; use PHPCompiler\JIT\HashTableHelper; use PHPCompiler\JIT\JitLongArg; @@ -49,6 +51,7 @@ public function execute(Frame $frame): void if ($argc >= 5) { $escape = VmReflection::stringArg($frame->calledArgs[4], 'fputcsv() escape', 4); } + VmCsvArg::validateFputcsvOptions($separator, $enclosure, $escape); $fields = []; foreach ($fieldsVar->toArray()->iterate(true) as $value) { $value = $value->resolveIndirect(); @@ -95,6 +98,11 @@ public function call(Context $context, JITVariable ...$args): Value if ($argc < 2 || $argc > 5) { throw new \LogicException('fputcsv() requires two to five arguments in this compiler build'); } + $compileTimeFailure = $this->emitCompileTimeCsvValidationFailure($context, $argc, ...$args); + if (null !== $compileTimeFailure) { + return $compileTimeFailure; + } + JitCsvArg::validateFputcsvCall($context, $argc, ...$args); $i64 = $context->getTypeFromString('int64'); $strPtr = $context->getTypeFromString('__string__*'); $handle = $context->builder->truncOrBitCast( @@ -135,4 +143,42 @@ private function loadFields(Context $context, JITVariable $arg): Value throw new \LogicException('fputcsv() fields must be an array in this compiler build'); } + + private function emitCompileTimeCsvValidationFailure(Context $context, int $argc, JITVariable ...$args): ?Value + { + $checks = [ + [3, 'separator', false, 2], + [4, 'enclosure', false, 3], + [5, 'escape', true, 4], + ]; + foreach ($checks as [$argNum, $paramName, $allowEmpty, $argIndex]) { + if ($argc < $argNum) { + continue; + } + $literal = $args[$argIndex]->compileTimeString ?? null; + if (null === $literal) { + continue; + } + $invalid = $allowEmpty ? \strlen($literal) > 1 : 1 !== \strlen($literal); + if (!$invalid) { + continue; + } + $message = $allowEmpty + ? \sprintf('fputcsv(): Argument #%d ($%s) must be empty or a single character', $argNum, $paramName) + : \sprintf('fputcsv(): Argument #%d ($%s) must be a single character', $argNum, $paramName); + TypeErrorRaise::registerDeclarations($context); + TypeErrorRaise::ensureLinked($context); + $errBlock = BasicBlockHelper::append($context, 'fputcsv_csv_lit_err_'.$argNum); + $afterBlock = BasicBlockHelper::append($context, 'fputcsv_csv_lit_after_'.$argNum); + $context->builder->branch($errBlock); + $context->builder->positionAtEnd($errBlock); + TypeErrorRaise::emitValueError($context, $message); + $context->builder->call($context->lookupFunction('abort')); + $context->builder->positionAtEnd($afterBlock); + + return $context->getTypeFromString('int64')->constInt(0, false); + } + + return null; + } } diff --git a/test/compliance/cases/stdlib/fputcsv_bad_handle.phpt b/test/compliance/cases/stdlib/fputcsv_bad_handle.phpt new file mode 100644 index 00000000000..1a9f31a5f3b --- /dev/null +++ b/test/compliance/cases/stdlib/fputcsv_bad_handle.phpt @@ -0,0 +1,11 @@ +--TEST-- +stdlib fputcsv() — bad stream handle TypeError (#4530, ext/standard/file.c) +--FILE-- +getMessage(), "\n"; +} +--EXPECT-- +TypeError: fputcsv(): Argument #1 ($stream) must be of type resource, int given diff --git a/test/compliance/cases/stdlib/fputcsv_escape.phpt b/test/compliance/cases/stdlib/fputcsv_escape.phpt new file mode 100644 index 00000000000..01809fe5044 --- /dev/null +++ b/test/compliance/cases/stdlib/fputcsv_escape.phpt @@ -0,0 +1,25 @@ +--TEST-- +stdlib fputcsv() — empty escape/enclosure ValueError parity (#4530, ext/standard/file.c) +--FILE-- + ''], + ['enclosure' => ''], + ['enclosure' => 'ab'], + ['separator' => ''], + ['escape' => 'ab'], +] as $case) { + try { + fputcsv($fp, ['a'], $case['separator'] ?? ',', $case['enclosure'] ?? '"', $case['escape'] ?? '\\'); + echo "no throw\n"; + } catch (Throwable $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; + } +} +--EXPECT-- +no throw +ValueError: fputcsv(): Argument #4 ($enclosure) must be a single character +ValueError: fputcsv(): Argument #4 ($enclosure) must be a single character +ValueError: fputcsv(): Argument #3 ($separator) must be a single character +ValueError: fputcsv(): Argument #5 ($escape) must be empty or a single character diff --git a/test/compliance/cases/stdlib/fputcsv_escape_jit.phpt b/test/compliance/cases/stdlib/fputcsv_escape_jit.phpt new file mode 100644 index 00000000000..5b99db099b3 --- /dev/null +++ b/test/compliance/cases/stdlib/fputcsv_escape_jit.phpt @@ -0,0 +1,14 @@ +--TEST-- +stdlib fputcsv() JIT — empty enclosure ValueError (#4530, ext/standard/file.c) +--JIT-- +--FILE-- +getMessage(), "\n"; +} +--EXPECT-- +ValueError: fputcsv(): Argument #4 ($enclosure) must be a single character diff --git a/test/fixtures/aot/compile-only/fputcsv_escape.php b/test/fixtures/aot/compile-only/fputcsv_escape.php new file mode 100644 index 00000000000..cf0bf55e2a2 --- /dev/null +++ b/test/fixtures/aot/compile-only/fputcsv_escape.php @@ -0,0 +1,11 @@ +getMessage(), "\n"; +} diff --git a/test/unit/FputcsvEscapeJitCompileTest.php b/test/unit/FputcsvEscapeJitCompileTest.php new file mode 100644 index 00000000000..3753506c67a --- /dev/null +++ b/test/unit/FputcsvEscapeJitCompileTest.php @@ -0,0 +1,41 @@ +assertNotFalse($bin); + $target = $root.'/test/fixtures/aot/compile-only/fputcsv_escape.php'; + $cmd = [PHP_BINARY, $bin, '-l', $target]; + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open($cmd, $descriptorSpec, $pipes, $root); + $this->assertIsResource($proc); + fclose($pipes[0]); + fclose($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + $exit = proc_close($proc); + $this->assertSame( + 0, + $exit, + trim($stderr !== false ? $stderr : '')."\n".'compile.php -l failed for fputcsv escape probe (#4530)' + ); + } +}