> */ private $arrays = [ [ 'expected' => [], 'input' => [0x0, 0x4], 'name' => 'empty', ], [ 'expected' => ['Foo'], 'input' => [0x1, 0x4, // Foo 0x43, 0x46, 0x6F, 0x6F, ], 'name' => 'one element', ], [ 'expected' => ['Foo', '人'], 'input' => [ 0x2, 0x4, // Foo 0x43, 0x46, 0x6F, 0x6F, // 人 0x43, 0xE4, 0xBA, 0xBA, ], 'name' => 'two elements', ], ]; /** * @var array> */ private $booleans = [ [ 'expected' => false, 'input' => [0x0, 0x7], ], [ 'expected' => true, 'input' => [0x1, 0x7], ], ]; /** * @var array> */ private $doubles = [ ['expected' => 0.0, 'input' => [0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]], ['expected' => 0.5, 'input' => [0x68, 0x3F, 0xE0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]], ['expected' => 3.14159265359, 'input' => [0x68, 0x40, 0x9, 0x21, 0xFB, 0x54, 0x44, 0x2E, 0xEA]], ['expected' => 123.0, 'input' => [0x68, 0x40, 0x5E, 0xC0, 0x0, 0x0, 0x0, 0x0, 0x0]], ['expected' => 1073741824.12457, 'input' => [0x68, 0x41, 0xD0, 0x0, 0x0, 0x0, 0x7, 0xF8, 0xF4]], ['expected' => -0.5, 'input' => [0x68, 0xBF, 0xE0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]], ['expected' => -3.14159265359, 'input' => [0x68, 0xC0, 0x9, 0x21, 0xFB, 0x54, 0x44, 0x2E, 0xEA]], ['expected' => -1073741824.12457, 'input' => [0x68, 0xC1, 0xD0, 0x0, 0x0, 0x0, 0x7, 0xF8, 0xF4]], ]; /** * @var array> */ private $floats = [ ['expected' => 0.0, 'input' => [0x4, 0x8, 0x0, 0x0, 0x0, 0x0]], ['expected' => 1.0, 'input' => [0x4, 0x8, 0x3F, 0x80, 0x0, 0x0]], ['expected' => 1.1, 'input' => [0x4, 0x8, 0x3F, 0x8C, 0xCC, 0xCD]], ['expected' => 3.14, 'input' => [0x4, 0x8, 0x40, 0x48, 0xF5, 0xC3]], ['expected' => 9999.99, 'input' => [0x4, 0x8, 0x46, 0x1C, 0x3F, 0xF6]], ['expected' => -1.0, 'input' => [0x4, 0x8, 0xBF, 0x80, 0x0, 0x0]], ['expected' => -1.1, 'input' => [0x4, 0x8, 0xBF, 0x8C, 0xCC, 0xCD]], ['expected' => -3.14, 'input' => [0x4, 0x8, 0xC0, 0x48, 0xF5, 0xC3]], ['expected' => -9999.99, 'input' => [0x4, 0x8, 0xC6, 0x1C, 0x3F, 0xF6]], ]; // PHP can't have arrays/objects as keys. Maybe redo all of the tests // this way so that we can use one test runner /** * @var array> */ private $maps = [ [ 'expected' => [], 'input' => [0xE0], 'name' => 'empty', ], [ 'expected' => ['en' => 'Foo'], 'input' => [0xE1, // en 0x42, 0x65, 0x6E, // Foo 0x43, 0x46, 0x6F, 0x6F, ], 'name' => 'one key', ], [ 'expected' => ['en' => 'Foo', 'zh' => '人'], 'input' => [ 0xE2, // en 0x42, 0x65, 0x6E, // Foo 0x43, 0x46, 0x6F, 0x6F, // zh 0x42, 0x7A, 0x68, // 人 0x43, 0xE4, 0xBA, 0xBA, ], 'name' => 'two keys', ], [ 'expected' => ['name' => ['en' => 'Foo', 'zh' => '人']], 'input' => [ 0xE1, // name 0x44, 0x6E, 0x61, 0x6D, 0x65, 0xE2, // en 0x42, 0x65, 0x6E, // Foo 0x43, 0x46, 0x6F, 0x6F, // zh 0x42, 0x7A, 0x68, // 人 0x43, 0xE4, 0xBA, 0xBA, ], 'name' => 'nested', ], [ 'expected' => ['languages' => ['en', 'zh']], 'input' => [ 0xE1, // languages 0x49, 0x6C, 0x61, 0x6E, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, // array 0x2, 0x4, // en 0x42, 0x65, 0x6E, // zh 0x42, 0x7A, 0x68, ], 'name' => 'map with array in it', ], ]; /** * @return array> */ private function pointers(): array { $v = [ ['expected' => 0, 'input' => [0x20, 0x0]], ['expected' => 5, 'input' => [0x20, 0x5]], ['expected' => 10, 'input' => [0x20, 0xA]], ['expected' => 1023, 'input' => [0x23, 0xFF]], ['expected' => 3017, 'input' => [0x28, 0x3, 0xC9]], ['expected' => 524283, 'input' => [0x2F, 0xF7, 0xFB]], ['expected' => 526335, 'input' => [0x2F, 0xFF, 0xFF]], ['expected' => 134217726, 'input' => [0x37, 0xF7, 0xF7, 0xFE]], ['expected' => 2147483647, 'input' => [0x38, 0x7F, 0xFF, 0xFF, 0xFF]], ]; if (\PHP_INT_MAX > 4294967295) { $v[] = ['expected' => 4294967295, 'input' => [0x38, 0xFF, 0xFF, 0xFF, 0xFF]]; } return $v; } /** * @var array> */ private $uint16 = [ ['expected' => 0, 'input' => [0xA0]], ['expected' => 255, 'input' => [0xA1, 0xFF]], ['expected' => 500, 'input' => [0xA2, 0x1, 0xF4]], ['expected' => 10872, 'input' => [0xA2, 0x2A, 0x78]], ['expected' => 65535, 'input' => [0xA2, 0xFF, 0xFF]], ]; /** * @var array> */ private $int32 = [ ['expected' => 0, 'input' => [0x0, 0x1]], ['expected' => -1, 'input' => [0x4, 0x1, 0xFF, 0xFF, 0xFF, 0xFF]], ['expected' => 255, 'input' => [0x1, 0x1, 0xFF]], ['expected' => -255, 'input' => [0x4, 0x1, 0xFF, 0xFF, 0xFF, 0x1]], ['expected' => 500, 'input' => [0x2, 0x1, 0x1, 0xF4]], ['expected' => -500, 'input' => [0x4, 0x1, 0xFF, 0xFF, 0xFE, 0xC]], ['expected' => 65535, 'input' => [0x2, 0x1, 0xFF, 0xFF]], ['expected' => -65535, 'input' => [0x4, 0x1, 0xFF, 0xFF, 0x0, 0x1]], ['expected' => 16777215, 'input' => [0x3, 0x1, 0xFF, 0xFF, 0xFF]], ['expected' => -16777215, 'input' => [0x4, 0x1, 0xFF, 0x0, 0x0, 0x1]], ['expected' => 2147483647, 'input' => [0x4, 0x1, 0x7F, 0xFF, 0xFF, 0xFF]], ['expected' => -2147483647, 'input' => [0x4, 0x1, 0x80, 0x0, 0x0, 0x1]], ]; /** * @return array> */ private function strings(): array { return [ ['expected' => '', 'input' => [0x40]], ['expected' => '1', 'input' => [0x41, 0x31]], ['expected' => '人', 'input' => [0x43, 0xE4, 0xBA, 0xBA]], ['expected' => '123', 'input' => [0x43, 0x31, 0x32, 0x33]], ['expected' => '123456789012345678901234567', 'input' => [0x5B, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, ]], ['expected' => '1234567890123456789012345678', 'input' => [0x5C, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, ]], ['expected' => '12345678901234567890123456789', 'input' => [0x5D, 0x0, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, ]], ['expected' => '123456789012345678901234567890', 'input' => [0x5D, 0x1, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, ]], ['expected' => str_repeat('x', 500), 'input' => array_pad( [0x5E, 0x0, 0xD7], 503, 0x78 ), ], [ 'expected' => str_repeat('x', 2000), 'input' => array_pad( [0x5E, 0x6, 0xB3], 2003, 0x78 ), ], [ 'expected' => str_repeat('x', 70000), 'input' => array_pad( [0x5F, 0x0, 0x10, 0x53], 70004, 0x78 ), ], ]; } /** * @return array> */ private function uint32(): array { return [ ['expected' => 0, 'input' => [0xC0]], ['expected' => 255, 'input' => [0xC1, 0xFF]], ['expected' => 500, 'input' => [0xC2, 0x1, 0xF4]], ['expected' => 10872, 'input' => [0xC2, 0x2A, 0x78]], ['expected' => 65535, 'input' => [0xC2, 0xFF, 0xFF]], ['expected' => 16777215, 'input' => [0xC3, 0xFF, 0xFF, 0xFF]], ['expected' => \PHP_INT_MAX < 4294967295 ? '4294967295' : 4294967295, 'input' => [0xC4, 0xFF, 0xFF, 0xFF, 0xFF]], ]; } /** * @return array> */ private function bytes(): array { // ugly deep clone $bytes = unserialize(serialize($this->strings())); foreach ($bytes as $key => $test) { $test['input'][0] ^= 0xC0; } return $bytes; } /** * @return array> */ public function generateLargeUint(int $bits): array { $ctrlByte = $bits === 64 ? 0x2 : 0x3; $uints = [ 0 => [0x0, $ctrlByte], 500 => [0x2, $ctrlByte, 0x1, 0xF4], 10872 => [0x2, $ctrlByte, 0x2A, 0x78], ]; for ($power = 1; $power <= $bits / 8; ++$power) { if (\extension_loaded('gmp')) { // This is to work around the limit added to gmp_pow here: // https://github.com/php/php-src/commit/e0a0e216a909dc4ee4ea7c113a5f41d49525f02e $v = 1; for ($i = 0; $i < $power; $i++) { $v = gmp_mul($v, 256); } $expected = gmp_strval(gmp_sub($v, '1')); } elseif (\extension_loaded('bcmath')) { $expected = bcsub(bcpow('2', (string) (8 * $power)), '1'); } else { $this->markTestSkipped('This test requires gmp or bcmath.'); } $input = [$power, $ctrlByte]; for ($i = 2; $i < 2 + $power; ++$i) { $input[$i] = 0xFF; } $uints[$expected] = $input; } return $uints; } public function testArrays(): void { $this->validateTypeDecodingList('array', $this->arrays); } public function testBooleans(): void { $this->validateTypeDecodingList('boolean', $this->booleans); } public function testBytes(): void { $this->validateTypeDecodingList('byte', $this->bytes()); } public function testDoubles(): void { $this->validateTypeDecodingList('double', $this->doubles); } public function testFloats(): void { $this->validateTypeDecodingList('float', $this->floats); } public function testInt32(): void { $this->validateTypeDecodingList('int32', $this->int32); } public function testMaps(): void { $this->validateTypeDecodingList('map', $this->maps); } public function testPointers(): void { $this->validateTypeDecodingList('pointers', $this->pointers()); } public function testStrings(): void { $this->validateTypeDecodingList('utf8_string', $this->strings()); } public function testUint16(): void { $this->validateTypeDecodingList('uint16', $this->uint16); } public function testUint32(): void { $this->validateTypeDecodingList('uint32', $this->uint32()); } public function testUint64(): void { $this->validateTypeDecoding('uint64', $this->generateLargeUint(64)); } public function testUint128(): void { $this->validateTypeDecoding('uint128', $this->generateLargeUint(128)); } /** * @param array $tests */ private function validateTypeDecoding(string $type, array $tests): void { foreach ($tests as $expected => $input) { $this->checkDecoding($type, $input, $expected); } } /** * @param array $tests */ private function validateTypeDecodingList(string $type, array $tests): void { foreach ($tests as $test) { $this->checkDecoding( $type, $test['input'], $test['expected'], $test['name'] ?? $test['input'] ); } } // @phpstan-ignore-next-line private function checkDecoding(string $type, array $input, $expected, $name = null): void { $name = $name || $expected; $description = "decoded $type - $name"; $handle = fopen('php://memory', 'rwb'); foreach ($input as $byte) { fwrite($handle, \chr($byte)); } fseek($handle, 0); $decoder = new Decoder($handle, 0, true); [$actual] = $decoder->decode(0); if ($type === 'float') { $actual = round($actual, 2); } $this->assertSame($expected, $actual, $description); } }