Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
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
[JsonStreamer] Various fixes and hardenings
  • Loading branch information
nicolas-grekas committed May 18, 2026
commit a305d44a369bbd70f66b90ea65b1734af660657b
1 change: 1 addition & 0 deletions 1 src/Symfony/Component/JsonStreamer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ CHANGELOG
* Deprecate `DateTimeToStringValueTransformer` and `StringToDateTimeValueTransformer`, use `DateTimeValueObjectTransformer` instead
* Deprecate `ValueTransformerInterface`, use `PropertyValueTransformerInterface` instead
* Add `$defaultOptions` to `JsonStreamReader` and `JsonStreamWriter`
* Reject JSON inputs reaching `Lexer::MAX_DEPTH` consistently with `json_decode()`

8.0
---
Expand Down
7 changes: 6 additions & 1 deletion 7 src/Symfony/Component/JsonStreamer/Read/Decoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\JsonStreamer\Read;

use Symfony\Component\JsonStreamer\Exception\RuntimeException;
use Symfony\Component\JsonStreamer\Exception\UnexpectedValueException;

/**
Expand All @@ -36,6 +37,10 @@ public static function decodeString(string $json): mixed
*/
public static function decodeStream($stream, int $offset = 0, ?int $length = null): mixed
{
return self::decodeString(stream_get_contents($stream, $length ?? -1, $offset));
if (false === $contents = stream_get_contents($stream, $length ?? -1, $offset)) {
throw new RuntimeException('Failed to read JSON stream.');
}

return self::decodeString($contents);
}
}
10 changes: 8 additions & 2 deletions 10 src/Symfony/Component/JsonStreamer/Read/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
final class Lexer
{
private const MAX_CHUNK_LENGTH = 8192;
private const MAX_DEPTH = 512;

private const WHITESPACE_CHARS = [' ' => true, "\r" => true, "\t" => true, "\n" => true];
private const STRUCTURE_CHARS = [',' => true, ':' => true, '{' => true, '}' => true, '[' => true, ']' => true];
Expand Down Expand Up @@ -165,7 +166,9 @@ private function validateToken(string $token, array &$context): void
throw new InvalidStreamException(\sprintf('Unexpected "%s" token.', $token));
}

++$context['pointer'];
if (++$context['pointer'] >= self::MAX_DEPTH - 1) {
throw new InvalidStreamException(\sprintf('Maximum stack depth of %d exceeded.', self::MAX_DEPTH));
}
$context['structures'][$context['pointer']] = 'dict';
$context['keys'][$context['pointer']] = [];
$context['expected_token'] = self::TOKEN_DICT_END | self::TOKEN_KEY;
Expand Down Expand Up @@ -195,8 +198,11 @@ private function validateToken(string $token, array &$context): void
throw new InvalidStreamException(\sprintf('Unexpected "%s" token.', $token));
}

if (++$context['pointer'] >= self::MAX_DEPTH - 1) {
throw new InvalidStreamException(\sprintf('Maximum stack depth of %d exceeded.', self::MAX_DEPTH));
}
$context['expected_token'] = self::TOKEN_LIST_END | self::TOKEN_VALUE;
$context['structures'][++$context['pointer']] = 'list';
$context['structures'][$context['pointer']] = 'list';

return;
}
Expand Down
38 changes: 17 additions & 21 deletions 38 src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function generate(DataModelNodeInterface $dataModel, bool $decodeFromStre
.$providers
.($this->canBeDecodedWithJsonDecode($dataModel, $decodeFromStream)
? $this->line(' return \\'.Decoder::class.'::decodeStream($stream, 0, null);', $context)
: $this->line(' return $providers['.$this->quote($dataModel->getIdentifier()).']($stream, 0, null);', $context))
: $this->line(' return $providers['.var_export($dataModel->getIdentifier(), true).']($stream, 0, null);', $context))
.$this->line('};', $context);
}

Expand All @@ -79,7 +79,7 @@ public function generate(DataModelNodeInterface $dataModel, bool $decodeFromStre
.$providers
.($this->canBeDecodedWithJsonDecode($dataModel, $decodeFromStream)
? $this->line(' return \\'.Decoder::class.'::decodeString((string) $string);', $context)
: $this->line(' return $providers['.$this->quote($dataModel->getIdentifier()).'](\\'.Decoder::class.'::decodeString((string) $string));', $context))
: $this->line(' return $providers['.var_export($dataModel->getIdentifier(), true).'](\\'.Decoder::class.'::decodeString((string) $string));', $context))
.$this->line('};', $context);
}

Expand All @@ -102,7 +102,7 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro
$accessor = $decodeFromStream ? '\\'.Decoder::class.'::decodeStream($stream, $offset, $length)' : '$data';
$arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data';

return $this->line('$providers['.$this->quote($node->getIdentifier())."] = static function ($arguments) {", $context)
return $this->line('$providers['.var_export($node->getIdentifier(), true)."] = static function ($arguments) {", $context)
.$this->line(' return '.$this->generateValueFormat($node, $accessor).';', $context)
.$this->line('};', $context);
}
Expand All @@ -117,20 +117,20 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro

$arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data';

$php .= $this->line('$providers['.$this->quote($node->getIdentifier())."] = static function ($arguments) use (\$options, \$transformers, \$instantiator, &\$providers) {", $context);
$php .= $this->line('$providers['.var_export($node->getIdentifier(), true)."] = static function ($arguments) use (\$options, \$transformers, \$instantiator, &\$providers) {", $context);

++$context['indentation_level'];

$php .= $decodeFromStream ? $this->line('$data = \\'.Decoder::class.'::decodeStream($stream, $offset, $length);', $context) : '';

foreach ($node->getNodes() as $n) {
$value = $this->canBeDecodedWithJsonDecode($n, $decodeFromStream) ? $this->generateValueFormat($n, '$data') : '$providers['.$this->quote($n->getIdentifier())."]($arguments)";
$value = $this->canBeDecodedWithJsonDecode($n, $decodeFromStream) ? $this->generateValueFormat($n, '$data') : '$providers['.var_export($n->getIdentifier(), true)."]($arguments)";
$php .= $this->line('if ('.$this->generateCompositeNodeItemCondition($n, '$data').') {', $context)
.$this->line(" return $value;", $context)
.$this->line('}', $context);
}

$php .= $this->line('throw new \\'.UnexpectedValueException::class.'(\\sprintf(\'Unexpected "%s" value for "%s".\', \\get_debug_type($data), '.$this->quote($node->getIdentifier()).'));', $context);
$php .= $this->line('throw new \\'.UnexpectedValueException::class.'(\\sprintf(\'Unexpected "%s" value for "%s".\', \\get_debug_type($data), '.var_export($node->getIdentifier(), true).'));', $context);

--$context['indentation_level'];

Expand All @@ -140,7 +140,7 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro
if ($node instanceof CollectionNode) {
$arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data';

$php = $this->line('$providers['.$this->quote($node->getIdentifier())."] = static function ($arguments) use (\$options, \$transformers, \$instantiator, &\$providers) {", $context);
$php = $this->line('$providers['.var_export($node->getIdentifier(), true)."] = static function ($arguments) use (\$options, \$transformers, \$instantiator, &\$providers) {", $context);

++$context['indentation_level'];

Expand All @@ -154,11 +154,11 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro
if ($decodeFromStream) {
$php .= $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream)
? $this->line(' yield $k => '.$this->generateValueFormat($node->getItemNode(), '\\'.Decoder::class.'::decodeStream($stream, $v[0], $v[1]);'), $context)
: $this->line(' yield $k => $providers['.$this->quote($node->getItemNode()->getIdentifier()).']($stream, $v[0], $v[1]);', $context);
: $this->line(' yield $k => $providers['.var_export($node->getItemNode()->getIdentifier(), true).']($stream, $v[0], $v[1]);', $context);
} else {
$php .= $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream)
? $this->line(' yield $k => $v;', $context)
: $this->line(' yield $k => $providers['.$this->quote($node->getItemNode()->getIdentifier()).']($v);', $context);
: $this->line(' yield $k => $providers['.var_export($node->getItemNode()->getIdentifier(), true).']($v);', $context);
}

$php .= $this->line(' }', $context)
Expand All @@ -183,13 +183,13 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro

$arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data';

$php = $this->line('$providers['.$this->quote($node->getIdentifier())."] = static function ($arguments) use (\$options, \$transformers, \$instantiator, &\$providers) {", $context);
$php = $this->line('$providers['.var_export($node->getIdentifier(), true)."] = static function ($arguments) use (\$options, \$transformers, \$instantiator, &\$providers) {", $context);

++$context['indentation_level'];

if ($valueObjectTransformerId = $this->getValueObjectTransformerId($node->getType()->getClassName())) {
$data = $decodeFromStream ? '\\'.Decoder::class.'::decodeStream($stream, $offset, $length)' : '$data';
$php .= $this->line("return \$transformers->get('$valueObjectTransformerId')->reverseTransform($data, \$options);", $context);
$php .= $this->line('return $transformers->get('.var_export($valueObjectTransformerId, true).")->reverseTransform($data, \$options);", $context);

--$context['indentation_level'];

Expand All @@ -206,9 +206,9 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro
foreach ($node->getProperties() as $streamedName => $property) {
$propertyValuePhp = $this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream)
? $this->generateValueFormat($property['value'], '\\'.Decoder::class.'::decodeStream($stream, $v[0], $v[1])')
: '$providers['.$this->quote($property['value']->getIdentifier()).']($stream, $v[0], $v[1])';
: '$providers['.var_export($property['value']->getIdentifier(), true).']($stream, $v[0], $v[1])';

$php .= $this->line(" '$streamedName' => \$object->".$property['name'].' = '.$property['accessor']($propertyValuePhp).',', $context);
$php .= $this->line(' '.var_export($streamedName, true).' => $object->'.$property['name'].' = '.$property['accessor']($propertyValuePhp).',', $context);
}

$php .= $this->line(' default => null,', $context)
Expand All @@ -219,10 +219,11 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro
$propertiesValuePhp = '[';
$separator = '';
foreach ($node->getProperties() as $streamedName => $property) {
$quotedStreamedName = var_export($streamedName, true);
$propertyValuePhp = $this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream)
? "\$data['$streamedName'] ?? '_symfony_missing_value'"
: "\\array_key_exists('$streamedName', \$data) ? \$providers[".$this->quote($property['value']->getIdentifier())."](\$data['$streamedName']) : '_symfony_missing_value'";
$propertiesValuePhp .= "$separator'".$property['name']."' => ".$property['accessor']($propertyValuePhp);
? "\$data[$quotedStreamedName] ?? '_symfony_missing_value'"
: "\\array_key_exists($quotedStreamedName, \$data) ? \$providers[".var_export($property['value']->getIdentifier(), true)."](\$data[$quotedStreamedName]) : '_symfony_missing_value'";
$propertiesValuePhp .= $separator.var_export($property['name'], true).' => '.$property['accessor']($propertyValuePhp);
$separator = ', ';
}
$propertiesValuePhp .= ']';
Expand Down Expand Up @@ -330,11 +331,6 @@ private function generateCompositeNodeItemCondition(DataModelNodeInterface $node
throw new LogicException(\sprintf('Unexpected "%s" type.', $type::class));
}

private function quote(string $identifier): string
{
return \sprintf("'%s'", addcslashes($identifier, "'"));
}

/**
* Determines if the $node can be decoded using a simple "json_decode".
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\EnumType;
use Symfony\Component\TypeInfo\Type\GenericType;
use Symfony\Component\TypeInfo\Type\IntersectionType;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\UnionType;

Expand Down Expand Up @@ -80,6 +81,10 @@ private function createDataModel(Type $type, array $options = [], array &$contex
{
$context['original_type'] ??= $type;

if ($type instanceof IntersectionType) {
throw new UnsupportedException(\sprintf('Intersection types are not supported ("%s").', (string) $type));
}

if ($type instanceof UnionType) {
return new CompositeNode(array_map(fn (Type $t): DataModelNodeInterface => $this->createDataModel($t, $options, $context), $type->getTypes()));
}
Expand Down Expand Up @@ -120,7 +125,7 @@ private function createDataModel(Type $type, array $options = [], array &$contex
'accessor' => static function (string $accessor) use ($propertyMetadata): string {
foreach ($propertyMetadata->getValueTransformers() as $valueTransformer) {
if (\is_string($valueTransformer)) {
$accessor = "\$transformers->get('$valueTransformer')->transform($accessor, \$options)";
$accessor = '$transformers->get('.var_export($valueTransformer, true).")->transform($accessor, \$options)";

continue;
}
Expand All @@ -131,6 +136,10 @@ private function createDataModel(Type $type, array $options = [], array &$contex
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
}

if ($functionReflection->isAnonymous()) {
throw new RuntimeException(\sprintf('Cannot generate accessor for anonymous function "%s".', $functionReflection->getName()));
}

$functionName = !$functionReflection->getClosureCalledClass()
? $functionReflection->getName()
: \sprintf('%s::%s', $functionReflection->getClosureCalledClass()->getName(), $functionReflection->getName());
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.