blob: b648deba7901af9c2939f6900329d751e54b7628 [file]
<?php
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
declare(strict_types=1);
namespace Test\Thrift\Integration\Lib\Protocol;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Thrift\Exception\TProtocolException;
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Protocol\TCompactProtocol;
use Thrift\Protocol\TJSONProtocol;
use Thrift\Protocol\TProtocol;
use Thrift\Transport\TMemoryBuffer;
use Thrift\Type\TType;
/**
* Round-trips the struct/union/exception recursion-depth limit through the
* generated read/write code (not the protocol counter in isolation), over a
* recursive struct (RecTree), union (RecUnion) and exception (RecError) from
* RecursionDepth.thrift.
*
* Both generated code paths are covered: the default mode emits an inline
* read/write loop guarded by the generator, while the "oop" mode delegates to
* TBase::readStruct/writeStruct which carry the guard. Round-tripping a chain
* of exactly DEFAULT_RECURSION_DEPTH levels also proves the two guards do not
* double-count (which would halve the effective limit).
*/
class RecursionDepthTest extends TestCase
{
private const LIMIT = TProtocol::DEFAULT_RECURSION_DEPTH; // 64
/**
* @return array<string, array{0: class-string<TProtocol>, 1: class-string, 2: string}>
*/
public static function caseProvider(): array
{
$modes = [
'inline' => ['\PhpRec\RecTree', '\PhpRec\RecUnion', '\PhpRec\RecError'],
'oop' => ['\PhpRecOop\RecTree', '\PhpRecOop\RecUnion', '\PhpRecOop\RecError'],
];
$protocols = [
'binary' => TBinaryProtocol::class,
'compact' => TCompactProtocol::class,
'json' => TJSONProtocol::class,
];
$cases = [];
foreach ($modes as $mode => [$treeClass, $unionClass, $errorClass]) {
foreach ($protocols as $protocol => $protocolClass) {
$cases["$mode/$protocol/struct"] = [$protocolClass, $treeClass, 'item'];
$cases["$mode/$protocol/union"] = [$protocolClass, $unionClass, 'leaf'];
$cases["$mode/$protocol/exception"] = [$protocolClass, $errorClass, 'leaf'];
}
}
return $cases;
}
#[DataProvider('caseProvider')]
public function testRoundTripsAtTheDepthLimit(string $protocolClass, string $class, string $leafField): void
{
$original = $this->makeChain($class, $leafField, self::LIMIT);
$buffer = new TMemoryBuffer();
$original->write(new $protocolClass($buffer));
$decoded = new $class();
$decoded->read(new $protocolClass(new TMemoryBuffer($buffer->getBuffer())));
$this->assertSame(self::LIMIT, $this->chainDepth($decoded));
}
#[DataProvider('caseProvider')]
public function testWritingPastTheDepthLimitThrows(string $protocolClass, string $class, string $leafField): void
{
$tooDeep = $this->makeChain($class, $leafField, self::LIMIT + 5);
$error = $this->captureThrowable(function () use ($tooDeep, $protocolClass) {
$tooDeep->write(new $protocolClass(new TMemoryBuffer()));
});
$this->assertInstanceOf(TProtocolException::class, $error);
$this->assertSame(TProtocolException::DEPTH_LIMIT, $error->getCode());
}
#[DataProvider('caseProvider')]
public function testReadingPastTheDepthLimitThrows(string $protocolClass, string $class, string $leafField): void
{
// Craft the over-limit payload with raw protocol primitives, which are
// not depth-bounded, so the generated reader recurses through the
// guarded struct path (field id 1 / list / struct), not skip().
$writeBuffer = new TMemoryBuffer();
$this->writeDeepStruct(new $protocolClass($writeBuffer), self::LIMIT + 5);
$readProtocol = new $protocolClass(new TMemoryBuffer($writeBuffer->getBuffer()));
$error = $this->captureThrowable(function () use ($readProtocol, $class) {
(new $class())->read($readProtocol);
});
$this->assertInstanceOf(TProtocolException::class, $error);
$this->assertSame(TProtocolException::DEPTH_LIMIT, $error->getCode());
}
private function makeChain(string $class, string $leafField, int $depth): object
{
$node = new $class([$leafField => 1]);
for ($i = 1; $i < $depth; $i++) {
$node = new $class(['children' => [$node]]);
}
return $node;
}
private function chainDepth(?object $node): int
{
$depth = 0;
while ($node !== null) {
$depth++;
$node = empty($node->children) ? null : $node->children[0];
}
return $depth;
}
/**
* Emit "$levels" nested structs whose recursive field (id 1, list<struct>)
* holds exactly one child, so a generated reader recurses $levels deep
* through the guarded struct path.
*/
private function writeDeepStruct(TProtocol $output, int $levels): void
{
for ($i = 0; $i < $levels - 1; $i++) {
$output->writeStructBegin('Rec');
$output->writeFieldBegin('children', TType::LST, 1);
$output->writeListBegin(TType::STRUCT, 1);
}
$output->writeStructBegin('Rec');
$output->writeFieldStop();
$output->writeStructEnd();
for ($i = 0; $i < $levels - 1; $i++) {
$output->writeListEnd();
$output->writeFieldEnd();
$output->writeFieldStop();
$output->writeStructEnd();
}
}
private function captureThrowable(callable $fn): ?\Throwable
{
try {
$fn();
} catch (\Throwable $e) {
return $e;
}
return null;
}
}