blob: 9b0ba6d083664cc4df80017bd5de9eb4cd22ba52 [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\Unit\Lib\Protocol;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\Thrift\Unit\Lib\ReflectionHelper;
use Thrift\Exception\TProtocolException;
use Thrift\Protocol\JSON\BaseContext;
use Thrift\Protocol\TJSONProtocol;
use Thrift\Transport\TMemoryBuffer;
use Thrift\Type\TMessageType;
use Thrift\Type\TType;
class TJSONProtocolTest extends TestCase
{
use ReflectionHelper;
#[DataProvider('writeAndReadMessageBeginDataProvider')]
public function testWriteAndReadMessageBegin(string $name, int $type, int $seqid)
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeMessageBegin($name, $type, $seqid);
$protocol->writeMessageEnd();
$protocol->reset();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$readName = null;
$readType = null;
$readSeqid = null;
$protocol->readMessageBegin($readName, $readType, $readSeqid);
$protocol->readMessageEnd();
$this->assertSame($name, $readName);
$this->assertSame($type, $readType);
$this->assertSame($seqid, $readSeqid);
}
public static function writeAndReadMessageBeginDataProvider()
{
yield 'call message' => [
'name' => 'testMethod',
'type' => TMessageType::CALL,
'seqid' => 1,
];
yield 'reply message' => [
'name' => 'getResult',
'type' => TMessageType::REPLY,
'seqid' => 42,
];
yield 'exception message' => [
'name' => 'failMethod',
'type' => TMessageType::EXCEPTION,
'seqid' => 100,
];
yield 'oneway message' => [
'name' => 'fireAndForget',
'type' => TMessageType::ONEWAY,
'seqid' => 0,
];
}
public function testReadMessageBeginBadVersion()
{
// Manually craft a JSON message with wrong version (99 instead of 1)
$json = '[99,"testMethod",1,1]';
$transport = new TMemoryBuffer($json);
$protocol = new TJSONProtocol($transport);
$this->expectException(TProtocolException::class);
$this->expectExceptionCode(TProtocolException::BAD_VERSION);
$name = null;
$type = null;
$seqid = null;
$protocol->readMessageBegin($name, $type, $seqid);
}
#[DataProvider('writeAndReadScalarDataProvider')]
public function testWriteAndReadScalar(int $fieldType, string $writeMethod, $value, string $readMethod)
{
if ($fieldType === TType::I64 && PHP_INT_SIZE === 4) {
$this->markTestSkipped('64-bit integer tests require 64-bit PHP');
}
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeStructBegin('Test');
$protocol->writeFieldBegin('f', $fieldType, 1);
$protocol->$writeMethod($value);
$protocol->writeFieldEnd();
$protocol->writeFieldStop();
$protocol->writeStructEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$protocol->readStructBegin($name);
$protocol->readFieldBegin($fname, $ftype, $fid);
$result = null;
$protocol->$readMethod($result);
$protocol->readFieldEnd();
$protocol->readStructEnd();
if (is_float($value) && is_nan($value)) {
$this->assertNan($result);
} else {
$this->assertSame($value, $result);
}
}
public static function writeAndReadScalarDataProvider()
{
yield 'bool true' => [
'fieldType' => TType::BOOL,
'writeMethod' => 'writeBool',
'value' => true,
'readMethod' => 'readBool',
];
yield 'bool false' => [
'fieldType' => TType::BOOL,
'writeMethod' => 'writeBool',
'value' => false,
'readMethod' => 'readBool',
];
yield 'byte zero' => [
'fieldType' => TType::BYTE,
'writeMethod' => 'writeByte',
'value' => 0,
'readMethod' => 'readByte',
];
yield 'byte positive' => [
'fieldType' => TType::BYTE,
'writeMethod' => 'writeByte',
'value' => 127,
'readMethod' => 'readByte',
];
yield 'byte negative' => [
'fieldType' => TType::BYTE,
'writeMethod' => 'writeByte',
'value' => -128,
'readMethod' => 'readByte',
];
yield 'byte one' => [
'fieldType' => TType::BYTE,
'writeMethod' => 'writeByte',
'value' => 1,
'readMethod' => 'readByte',
];
yield 'i16 zero' => [
'fieldType' => TType::I16,
'writeMethod' => 'writeI16',
'value' => 0,
'readMethod' => 'readI16',
];
yield 'i16 positive' => [
'fieldType' => TType::I16,
'writeMethod' => 'writeI16',
'value' => 32767,
'readMethod' => 'readI16',
];
yield 'i16 negative' => [
'fieldType' => TType::I16,
'writeMethod' => 'writeI16',
'value' => -32768,
'readMethod' => 'readI16',
];
yield 'i16 small positive' => [
'fieldType' => TType::I16,
'writeMethod' => 'writeI16',
'value' => 256,
'readMethod' => 'readI16',
];
yield 'i32 zero' => [
'fieldType' => TType::I32,
'writeMethod' => 'writeI32',
'value' => 0,
'readMethod' => 'readI32',
];
yield 'i32 positive' => [
'fieldType' => TType::I32,
'writeMethod' => 'writeI32',
'value' => 2147483647,
'readMethod' => 'readI32',
];
yield 'i32 negative' => [
'fieldType' => TType::I32,
'writeMethod' => 'writeI32',
'value' => -2147483648,
'readMethod' => 'readI32',
];
yield 'i32 small negative' => [
'fieldType' => TType::I32,
'writeMethod' => 'writeI32',
'value' => -1,
'readMethod' => 'readI32',
];
yield 'i32 medium value' => [
'fieldType' => TType::I32,
'writeMethod' => 'writeI32',
'value' => 100000,
'readMethod' => 'readI32',
];
yield 'i64 zero' => [
'fieldType' => TType::I64,
'writeMethod' => 'writeI64',
'value' => 0,
'readMethod' => 'readI64',
];
yield 'i64 positive' => [
'fieldType' => TType::I64,
'writeMethod' => 'writeI64',
'value' => 1099511627776,
'readMethod' => 'readI64',
];
yield 'i64 negative' => [
'fieldType' => TType::I64,
'writeMethod' => 'writeI64',
'value' => -1099511627776,
'readMethod' => 'readI64',
];
yield 'i64 max int32' => [
'fieldType' => TType::I64,
'writeMethod' => 'writeI64',
'value' => 2147483647,
'readMethod' => 'readI64',
];
yield 'i64 small value' => [
'fieldType' => TType::I64,
'writeMethod' => 'writeI64',
'value' => 42,
'readMethod' => 'readI64',
];
yield 'double zero' => [
'fieldType' => TType::DOUBLE,
'writeMethod' => 'writeDouble',
'value' => 0.0,
'readMethod' => 'readDouble',
];
yield 'double positive' => [
'fieldType' => TType::DOUBLE,
'writeMethod' => 'writeDouble',
'value' => 3.14159265358979,
'readMethod' => 'readDouble',
];
yield 'double negative' => [
'fieldType' => TType::DOUBLE,
'writeMethod' => 'writeDouble',
'value' => -2.718281828,
'readMethod' => 'readDouble',
];
yield 'double large' => [
'fieldType' => TType::DOUBLE,
'writeMethod' => 'writeDouble',
'value' => 1.7976931348623e+100,
'readMethod' => 'readDouble',
];
yield 'double small' => [
'fieldType' => TType::DOUBLE,
'writeMethod' => 'writeDouble',
'value' => 1.0e-10,
'readMethod' => 'readDouble',
];
yield 'double NaN round-trips via "NaN" token' => [
'fieldType' => TType::DOUBLE,
'writeMethod' => 'writeDouble',
'value' => NAN,
'readMethod' => 'readDouble',
];
yield 'double +Infinity round-trips via "Infinity" token' => [
'fieldType' => TType::DOUBLE,
'writeMethod' => 'writeDouble',
'value' => INF,
'readMethod' => 'readDouble',
];
yield 'double -Infinity round-trips via "-Infinity" token' => [
'fieldType' => TType::DOUBLE,
'writeMethod' => 'writeDouble',
'value' => -INF,
'readMethod' => 'readDouble',
];
yield 'string empty' => [
'fieldType' => TType::STRING,
'writeMethod' => 'writeString',
'value' => '',
'readMethod' => 'readString',
];
yield 'string simple' => [
'fieldType' => TType::STRING,
'writeMethod' => 'writeString',
'value' => 'hello world',
'readMethod' => 'readString',
];
yield 'string special characters' => [
'fieldType' => TType::STRING,
'writeMethod' => 'writeString',
'value' => "line1\nline2\ttab",
'readMethod' => 'readString',
];
yield 'string unicode' => [
'fieldType' => TType::STRING,
'writeMethod' => 'writeString',
'value' => 'héllo wörld',
'readMethod' => 'readString',
];
yield 'string quotes and backslash' => [
'fieldType' => TType::STRING,
'writeMethod' => 'writeString',
'value' => 'say "hello" and use \\path',
'readMethod' => 'readString',
];
yield 'string json special chars' => [
'fieldType' => TType::STRING,
'writeMethod' => 'writeString',
'value' => '{"key": "value"}',
'readMethod' => 'readString',
];
yield 'uuid standard' => [
'fieldType' => TType::UUID,
'writeMethod' => 'writeUuid',
'value' => '12345678-1234-5678-1234-567812345678',
'readMethod' => 'readUuid',
];
yield 'uuid nil' => [
'fieldType' => TType::UUID,
'writeMethod' => 'writeUuid',
'value' => '00000000-0000-0000-0000-000000000000',
'readMethod' => 'readUuid',
];
yield 'uuid random' => [
'fieldType' => TType::UUID,
'writeMethod' => 'writeUuid',
'value' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'readMethod' => 'readUuid',
];
}
public function testWriteAndReadStruct()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeStructBegin('TestStruct');
$protocol->writeFieldBegin('name', TType::STRING, 1);
$protocol->writeString('hello');
$protocol->writeFieldEnd();
$protocol->writeFieldBegin('age', TType::I32, 2);
$protocol->writeI32(25);
$protocol->writeFieldEnd();
$protocol->writeFieldStop();
$protocol->writeStructEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$name = null;
$protocol->readStructBegin($name);
$fieldName = null;
$fieldType = null;
$fieldId = null;
$protocol->readFieldBegin($fieldName, $fieldType, $fieldId);
$this->assertSame(TType::STRING, $fieldType);
$this->assertSame(1, $fieldId);
$str = null;
$protocol->readString($str);
$this->assertSame('hello', $str);
$protocol->readFieldEnd();
$protocol->readFieldBegin($fieldName, $fieldType, $fieldId);
$this->assertSame(TType::I32, $fieldType);
$this->assertSame(2, $fieldId);
$i32 = null;
$protocol->readI32($i32);
$this->assertSame(25, $i32);
$protocol->readFieldEnd();
// Read field stop
$protocol->readFieldBegin($fieldName, $fieldType, $fieldId);
$this->assertSame(TType::STOP, $fieldType);
$protocol->readStructEnd();
}
public function testWriteAndReadMap()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeMapBegin(TType::STRING, TType::I32, 2);
$protocol->writeString('key1');
$protocol->writeI32(100);
$protocol->writeString('key2');
$protocol->writeI32(200);
$protocol->writeMapEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$keyType = null;
$valType = null;
$size = null;
$protocol->readMapBegin($keyType, $valType, $size);
$this->assertSame(TType::STRING, $keyType);
$this->assertSame(TType::I32, $valType);
$this->assertSame(2, $size);
$key = null;
$val = null;
$protocol->readString($key);
$protocol->readI32($val);
$this->assertSame('key1', $key);
$this->assertSame(100, $val);
$protocol->readString($key);
$protocol->readI32($val);
$this->assertSame('key2', $key);
$this->assertSame(200, $val);
$protocol->readMapEnd();
}
public function testWriteAndReadList()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeListBegin(TType::I32, 3);
$protocol->writeI32(10);
$protocol->writeI32(20);
$protocol->writeI32(30);
$protocol->writeListEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$elemType = null;
$size = null;
$protocol->readListBegin($elemType, $size);
$this->assertSame(TType::I32, $elemType);
$this->assertSame(3, $size);
$val = null;
$protocol->readI32($val);
$this->assertSame(10, $val);
$protocol->readI32($val);
$this->assertSame(20, $val);
$protocol->readI32($val);
$this->assertSame(30, $val);
$protocol->readListEnd();
}
public function testWriteAndReadSet()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeSetBegin(TType::STRING, 2);
$protocol->writeString('alpha');
$protocol->writeString('beta');
$protocol->writeSetEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$elemType = null;
$size = null;
$protocol->readSetBegin($elemType, $size);
$this->assertSame(TType::STRING, $elemType);
$this->assertSame(2, $size);
$val = null;
$protocol->readString($val);
$this->assertSame('alpha', $val);
$protocol->readString($val);
$this->assertSame('beta', $val);
$protocol->readSetEnd();
}
public function testWriteAndReadEmptyMap()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeMapBegin(TType::STRING, TType::I32, 0);
$protocol->writeMapEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$keyType = null;
$valType = null;
$size = null;
$protocol->readMapBegin($keyType, $valType, $size);
$this->assertSame(TType::STRING, $keyType);
$this->assertSame(TType::I32, $valType);
$this->assertSame(0, $size);
$protocol->readMapEnd();
}
public function testWriteAndReadEmptyList()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeListBegin(TType::BOOL, 0);
$protocol->writeListEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$elemType = null;
$size = null;
$protocol->readListBegin($elemType, $size);
$this->assertSame(TType::BOOL, $elemType);
$this->assertSame(0, $size);
$protocol->readListEnd();
}
public function testGetTypeNameForUnknownType()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$this->expectException(TProtocolException::class);
$this->expectExceptionCode(TProtocolException::UNKNOWN);
// Use writeFieldBegin with an invalid type to trigger getTypeNameForTypeID
$protocol->writeFieldBegin('invalid', 99, 1);
}
public function testGetTypeIDForUnknownTypeName()
{
// Craft JSON that has an unknown type name in field position
// A struct with one field: fieldId=1, then object start, then unknown type name
$json = '{1:{"zzz"';
$transport = new TMemoryBuffer($json);
$protocol = new TJSONProtocol($transport);
$this->expectException(TProtocolException::class);
$this->expectExceptionCode(TProtocolException::INVALID_DATA);
$name = null;
$protocol->readStructBegin($name);
$fieldName = null;
$fieldType = null;
$fieldId = null;
$protocol->readFieldBegin($fieldName, $fieldType, $fieldId);
}
public function testWriteAndReadCompleteMessage()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
// Write a complete message with a struct containing various types
$protocol->writeMessageBegin('testFunc', TMessageType::CALL, 7);
$protocol->writeStructBegin('Args');
$protocol->writeFieldBegin('flag', TType::BOOL, 1);
$protocol->writeBool(true);
$protocol->writeFieldEnd();
$protocol->writeFieldBegin('count', TType::I32, 2);
$protocol->writeI32(42);
$protocol->writeFieldEnd();
$protocol->writeFieldStop();
$protocol->writeStructEnd();
$protocol->writeMessageEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$name = null;
$type = null;
$seqid = null;
$protocol->readMessageBegin($name, $type, $seqid);
$this->assertSame('testFunc', $name);
$this->assertSame(TMessageType::CALL, $type);
$this->assertSame(7, $seqid);
$structName = null;
$protocol->readStructBegin($structName);
$fieldName = null;
$fieldType = null;
$fieldId = null;
$protocol->readFieldBegin($fieldName, $fieldType, $fieldId);
$this->assertSame(TType::BOOL, $fieldType);
$this->assertSame(1, $fieldId);
$boolVal = null;
$protocol->readBool($boolVal);
$this->assertTrue($boolVal);
$protocol->readFieldEnd();
$protocol->readFieldBegin($fieldName, $fieldType, $fieldId);
$this->assertSame(TType::I32, $fieldType);
$this->assertSame(2, $fieldId);
$i32Val = null;
$protocol->readI32($i32Val);
$this->assertSame(42, $i32Val);
$protocol->readFieldEnd();
$protocol->readFieldBegin($fieldName, $fieldType, $fieldId);
$this->assertSame(TType::STOP, $fieldType);
$protocol->readStructEnd();
$protocol->readMessageEnd();
}
public function testWriteAndReadNestedList()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
// Write a list of lists
$protocol->writeListBegin(TType::LST, 2);
$protocol->writeListBegin(TType::I32, 2);
$protocol->writeI32(1);
$protocol->writeI32(2);
$protocol->writeListEnd();
$protocol->writeListBegin(TType::I32, 2);
$protocol->writeI32(3);
$protocol->writeI32(4);
$protocol->writeListEnd();
$protocol->writeListEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$elemType = null;
$size = null;
$protocol->readListBegin($elemType, $size);
$this->assertSame(TType::LST, $elemType);
$this->assertSame(2, $size);
$protocol->readListBegin($elemType, $size);
$this->assertSame(TType::I32, $elemType);
$this->assertSame(2, $size);
$val = null;
$protocol->readI32($val);
$this->assertSame(1, $val);
$protocol->readI32($val);
$this->assertSame(2, $val);
$protocol->readListEnd();
$protocol->readListBegin($elemType, $size);
$this->assertSame(TType::I32, $elemType);
$this->assertSame(2, $size);
$protocol->readI32($val);
$this->assertSame(3, $val);
$protocol->readI32($val);
$this->assertSame(4, $val);
$protocol->readListEnd();
$protocol->readListEnd();
}
public function testWriteAndReadMapWithIntegerKeys()
{
$transport = new TMemoryBuffer();
$protocol = new TJSONProtocol($transport);
$protocol->writeMapBegin(TType::I32, TType::STRING, 2);
$protocol->writeI32(1);
$protocol->writeString('one');
$protocol->writeI32(2);
$protocol->writeString('two');
$protocol->writeMapEnd();
$transport = new TMemoryBuffer($transport->getBuffer());
$protocol = new TJSONProtocol($transport);
$keyType = null;
$valType = null;
$size = null;
$protocol->readMapBegin($keyType, $valType, $size);
$this->assertSame(TType::I32, $keyType);
$this->assertSame(TType::STRING, $valType);
$this->assertSame(2, $size);
$key = null;
$val = null;
$protocol->readI32($key);
$protocol->readString($val);
$this->assertSame(1, $key);
$this->assertSame('one', $val);
$protocol->readI32($key);
$protocol->readString($val);
$this->assertSame(2, $key);
$this->assertSame('two', $val);
$protocol->readMapEnd();
}
/**
* Guards the popContext underflow recovery added when the `$context`
* property was tightened to non-nullable: an empty stack now yields a
* default BaseContext instead of returning null and tripping the typed
* property assignment.
*/
public function testPopContextOnEmptyStackFallsBackToBaseContext(): void
{
$protocol = new TJSONProtocol(new TMemoryBuffer());
// Reflect into the private popContext()/context to assert the
// underflow path completes without TypeError.
$popContext = $this->getAccessibleMethod($protocol, 'popContext');
$popContext->invoke($protocol);
$context = $this->getPropertyValue($protocol, 'context');
$this->assertInstanceOf(BaseContext::class, $context);
}
}