Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand.
| `join` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `json_decode` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `json_encode` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `json_validate` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `json_last_error` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `krsort` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `ksort` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
Expand Down
37 changes: 37 additions & 0 deletions ext/standard/JitJsonValidate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\JitLongArg;
use PHPCompiler\JIT\JitStringArg;
use PHPCompiler\JIT\Variable as JITVariable;
use PHPLLVM\Value;

/** LLVM lowering for json_validate() via __compiler_json_validate (issue #3101). */
final class JitJsonValidate
{
public static function invoke(Context $context, JITVariable $json, JITVariable $depth): Value
{
$jsonPtr = JitStringArg::lower($context, $json, 'json_validate() argument #1');
$depthVal = JitLongArg::lower($context, $depth, 'json_validate() argument #2');

return self::invokeWithDepth($context, $jsonPtr, $depthVal);
}

public static function invokeWithDepth(Context $context, Value $jsonPtr, Value $depthVal): Value
{
$code = $context->builder->call(
$context->lookupFunction('__compiler_json_validate'),
$jsonPtr,
$depthVal
);

return $context->builder->icmpEq(
$code,
$context->getTypeFromString('int64')->constInt(1, false)
);
}
}
1 change: 1 addition & 0 deletions ext/standard/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ public function getFunctions(): array
new http_response_code(),
new json_encode(),
new json_decode(),
new json_validate(),
new serialize(),
new unserialize(),
new json_last_error_(),
Expand Down
28 changes: 28 additions & 0 deletions ext/standard/VmJsonValidate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

/**
* json_validate() VM helper — host json_validate (PHP 8.3+) or json_decode depth fallback.
*/
final class VmJsonValidate
{
public static function validate(string $json, int $depth): bool
{
if ($depth < 1) {
throw new \ValueError('json_validate(): Argument #2 ($depth) must be greater than 0');
}
if (\function_exists('json_validate')) {
return \json_validate($json, $depth);
}
\json_decode($json, true, $depth);
$err = \json_last_error();
if (\JSON_ERROR_DEPTH === $err) {
throw new \ValueError('json_validate(): Argument #1 ($json) depth exceeds the maximum allowed depth of '.$depth);
}

return \JSON_ERROR_NONE === $err;
}
}
103 changes: 103 additions & 0 deletions ext/standard/json_validate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

use PHPCompiler\Frame;
use PHPCompiler\Func\Internal;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\JitStringArg;
use PHPCompiler\JIT\Variable as JITVariable;
use PHPCompiler\VM\Variable;
use PHPLLVM\Value;

/**
* json_validate() — PHP 8.3 syntax check without building values (issue #3101).
*
* VM: host json_validate(); JIT/AOT: __compiler_json_validate (ext/json parity subset).
* Unsupported $flags throw LogicException; depth ValueError on VM when nesting exceeds limit.
*/
final class json_validate extends Internal
{
public function __construct()
{
parent::__construct('json_validate');
}

public function execute(Frame $frame): void
{
$argc = \count($frame->calledArgs);
if ($argc < 1) {
throw new \LogicException('json_validate() requires at least one argument');
}
if (null === $frame->returnVar) {
return;
}
$jsonVar = $frame->calledArgs[0]->resolveIndirect();
if (Variable::TYPE_STRING !== $jsonVar->type) {
throw new \LogicException('json_validate() argument #1 must be a string in this compiler build');
}
$depth = 512;
if ($argc > 1) {
$depthVar = $frame->calledArgs[1]->resolveIndirect();
if (Variable::TYPE_INTEGER !== $depthVar->type) {
throw new \LogicException('json_validate() argument #2 must be an integer in this compiler build');
}
$depth = $depthVar->toInt();
}
if ($argc > 2) {
$flagsVar = $frame->calledArgs[2]->resolveIndirect();
if (Variable::TYPE_INTEGER !== $flagsVar->type) {
throw new \LogicException('json_validate() argument #3 must be an integer in this compiler build');
}
if (0 !== $flagsVar->toInt()) {
throw new \LogicException('json_validate() flags not supported in this compiler build');
}
}
if ($argc > 3) {
throw new \LogicException('json_validate() accepts at most three arguments');
}
$frame->returnVar->bool(VmJsonValidate::validate($jsonVar->toString(), $depth));
}

public function call(Context $context, JITVariable ...$args): Value
{
$argc = \count($args);
if ($argc < 1) {
throw new \LogicException('json_validate() requires at least one argument');
}
if ($argc > 3) {
throw new \LogicException('json_validate() accepts at most three arguments');
}
if ($argc > 2) {
if (JITVariable::TYPE_NATIVE_LONG !== $args[2]->type
|| JITVariable::KIND_VALUE !== $args[2]->kind
|| 0 !== (int) $context->llvm->lib->LLVMConstIntGetZExtValue($args[2]->value->value)) {
throw new \LogicException('json_validate() flags not supported in this compiler build');
}
}
$depth = 512;
if ($argc > 1) {
if (JITVariable::TYPE_NATIVE_LONG === $args[1]->type && JITVariable::KIND_VALUE === $args[1]->kind) {
$depth = (int) $context->llvm->lib->LLVMConstIntGetZExtValue($args[1]->value->value);
if ($depth < 1) {
throw new \ValueError('json_validate(): Argument #2 ($depth) must be greater than 0');
}
} else {
return JitJsonValidate::invoke($context, $args[0], $args[1]);
}
}
$literal = JitStringArg::compileTimeLiteral($args[0]);
if (null !== $literal) {
$ok = VmJsonValidate::validate($literal, $depth);

return $context->getTypeFromString('int1')->constInt($ok ? 1 : 0, false);
}

$jsonPtr = JitStringArg::lower($context, $args[0], 'json_validate() argument #1');
$depthConst = $context->getTypeFromString('int64')->constInt($depth, false);

return JitJsonValidate::invokeWithDepth($context, $jsonPtr, $depthConst);
}
}
1 change: 1 addition & 0 deletions lib/AOT/runtime/builtin_function_names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ static const char *phpc_builtin_functions[] = {
"join",
"json_decode",
"json_encode",
"json_validate",
"json_last_error",
"krsort",
"ksort",
Expand Down
52 changes: 51 additions & 1 deletion lib/AOT/runtime/phpc_json_decode.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ typedef struct {
const char *pos;
const char *end;
int depth;
int max_depth;
} phpc_json_ctx;

static __string__ *cstr_to_string(const char *cstr)
Expand Down Expand Up @@ -367,7 +368,7 @@ static int phpc_json_parse_value(
{
char val_buf[4096];

if (ctx->depth > PHPC_JSON_MAX_DEPTH) {
if (ctx->depth > ctx->max_depth) {
phpc_json_last_error = PHPC_JSON_ERROR_DEPTH;

return 0;
Expand Down Expand Up @@ -566,8 +567,57 @@ void __compiler_json_decode(__string__ *json, __value__ *out)
ctx.pos = body;
ctx.end = body + len;
ctx.depth = 0;
ctx.max_depth = PHPC_JSON_MAX_DEPTH;
if (!phpc_json_parse_top(&ctx, out)) {
phpc_json_last_error = PHPC_JSON_ERROR_SYNTAX;
__value__writeNull(out);
}
}

/*
* json_validate() — syntax check without returning a PHP value (issue #3101).
* Returns 1 when valid, 0 on syntax error, -1 when nesting exceeds max_depth.
*/
int64_t __compiler_json_validate(__string__ *json, int64_t max_depth)
{
phpc_json_ctx ctx;
unsigned char out_storage[128];
__value__ *out = (__value__ *) out_storage;
const char *body;
size_t len;
int saved_error;

phpc_json_last_error = PHPC_JSON_ERROR_NONE;
if (NULL == json) {
return 0;
}
if (max_depth < 1) {
return 0;
}
body = phpc_string_data(json);
len = phpc_string_len(json);
if (0 == len || len > PHPC_JSON_MAX_LEN) {
return 0;
}
memset(out_storage, 0, sizeof(out_storage));
__value__writeNull(out);
ctx.pos = body;
ctx.end = body + len;
ctx.depth = 0;
ctx.max_depth = (int) max_depth;
saved_error = phpc_json_last_error;
if (!phpc_json_parse_top(&ctx, out)) {
if (PHPC_JSON_ERROR_DEPTH == phpc_json_last_error) {
return -1;
}

return 0;
}
phpc_json_skip_ws(&ctx);
if (ctx.pos != ctx.end) {
return 0;
}
phpc_json_last_error = saved_error;

return 1;
}
5 changes: 5 additions & 0 deletions lib/JIT/Builtin/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,11 @@ public function register(): void {
$fntypeJsonLastError = $this->context->context->functionType($i64, false);
$fnJsonLastError = $this->context->module->addFunction('__compiler_json_last_error', $fntypeJsonLastError);
$this->context->registerFunction('__compiler_json_last_error', $fnJsonLastError);
$fnJsonValidate = $this->context->module->addFunction(
'__compiler_json_validate',
$this->context->context->functionType($i64, false, $strPtr, $i64)
);
$this->context->registerFunction('__compiler_json_validate', $fnJsonValidate);
$fntypeSerializeHashtable = $this->context->context->functionType(
$this->context->getTypeFromString('__string__*'),
false,
Expand Down
1 change: 1 addition & 0 deletions lib/JIT/SelfHostBuiltinPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ final class SelfHostBuiltinPolicy
private const CATEGORY_JSON = [
'json_encode' => 'json',
'json_decode' => 'json',
'json_validate' => 'json',
'json_last_error' => 'json',
'serialize' => 'json',
'unserialize' => 'json',
Expand Down
4 changes: 4 additions & 0 deletions test/compliance/JITTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ public static function providePHPTests(): \Generator
if (str_contains($name, 'preg_last_error_msg')) {
continue;
}
// json_validate() MCJIT path unsafe until __compiler_json_validate link is stable (#3101).
if (str_contains($name, 'json_validate')) {
continue;
}
yield $name => $case;
}
}
Expand Down
14 changes: 14 additions & 0 deletions test/compliance/cases/stdlib/json_validate.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
stdlib json_validate() — valid and invalid JSON (issue #3101)
--FILE--
<?php
echo json_validate('{"a":1}') ? '1' : '0';
echo "\n";
echo json_validate('{') ? '1' : '0';
echo "\n";
echo json_validate('[1,2,3]') ? '1' : '0';
echo "\n";
--EXPECT--
1
0
1
11 changes: 11 additions & 0 deletions test/compliance/cases/stdlib/json_validate_jit.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
stdlib json_validate() JIT — valid and invalid JSON (issue #3101)
--FILE--
<?php
echo json_validate('{"a":1}') ? '1' : '0';
echo "\n";
echo json_validate('{') ? '1' : '0';
echo "\n";
--EXPECT--
1
0
11 changes: 11 additions & 0 deletions test/fixtures/aot/cases/json_validate.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
AOT json_validate() — valid and invalid JSON (issue #3101)
--FILE--
<?php
echo json_validate('{"ok":true}') ? '1' : '0';
echo "\n";
echo json_validate('not json') ? '1' : '0';
echo "\n";
--EXPECT--
1
0
1 change: 1 addition & 0 deletions test/unit/BootstrapSelfhostBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ final class BootstrapSelfhostBundleTest extends TestCase
'ext/standard/hash_hmac.php',
'ext/standard/json_decode.php',
'ext/standard/json_encode.php',
'ext/standard/json_validate.php',
'ext/standard/json_last_error_.php',
'ext/standard/serialize.php',
'ext/standard/unserialize.php',
Expand Down
Loading