LoginPacket: use netresearch/jsonmapper for login data decoding

this makes retrieval static analysis friendly without extra steps.
This commit is contained in:
Dylan K. Taylor 2020-03-23 21:58:12 +00:00
parent 3e5d3a646b
commit 83a3adecff
11 changed files with 495 additions and 143 deletions

View File

@ -41,7 +41,8 @@
"pocketmine/classloader": "dev-master", "pocketmine/classloader": "dev-master",
"pocketmine/callback-validator": "^1.0.1", "pocketmine/callback-validator": "^1.0.1",
"adhocore/json-comment": "^0.1.0", "adhocore/json-comment": "^0.1.0",
"particle/validator": "^2.3" "particle/validator": "^2.3",
"netresearch/jsonmapper": "^2.0"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^0.12.14", "phpstan/phpstan": "^0.12.14",

48
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b59b043a71525b45752770b4fd1ce2cb", "content-hash": "8a94ad4a1822c04cb884a6dee81923df",
"packages": [ "packages": [
{ {
"name": "adhocore/json-comment", "name": "adhocore/json-comment",
@ -191,6 +191,52 @@
], ],
"time": "2018-12-03T18:17:01+00:00" "time": "2018-12-03T18:17:01+00:00"
}, },
{
"name": "netresearch/jsonmapper",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/cweiske/jsonmapper.git",
"reference": "e245890383c3ed38b6d202ee373c23ccfebc0f54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e245890383c3ed38b6d202ee373c23ccfebc0f54",
"reference": "e245890383c3ed38b6d202ee373c23ccfebc0f54",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=5.6"
},
"require-dev": {
"phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0",
"squizlabs/php_codesniffer": "~3.5"
},
"type": "library",
"autoload": {
"psr-0": {
"JsonMapper": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"OSL-3.0"
],
"authors": [
{
"name": "Christian Weiske",
"email": "cweiske@cweiske.de",
"homepage": "http://github.com/cweiske/jsonmapper/",
"role": "Developer"
}
],
"description": "Map nested JSON structures onto PHP classes",
"time": "2020-03-04T17:23:33+00:00"
},
{ {
"name": "particle/validator", "name": "particle/validator",
"version": "v2.3.4", "version": "v2.3.4",

View File

@ -33,6 +33,7 @@ parameters:
- pocketmine\IS_DEVELOPMENT_BUILD - pocketmine\IS_DEVELOPMENT_BUILD
- pocketmine\DEBUG - pocketmine\DEBUG
stubFiles: stubFiles:
- tests/phpstan/stubs/JsonMapper.stub
- tests/phpstan/stubs/pthreads.stub - tests/phpstan/stubs/pthreads.stub
reportUnmatchedIgnoredErrors: false #no other way to silence platform-specific non-warnings reportUnmatchedIgnoredErrors: false #no other way to silence platform-specific non-warnings
ignoreErrors: ignoreErrors:

View File

@ -95,7 +95,7 @@ class ProcessLoginTask extends AsyncTask{
$currentKey = null; $currentKey = null;
$first = true; $first = true;
foreach($packet->chainDataJwt as $jwt){ foreach($packet->chainDataJwt->chain as $jwt){
$this->validateToken($jwt, $currentKey, $first); $this->validateToken($jwt, $currentKey, $first);
if($first){ if($first){
$first = false; $first = false;

View File

@ -68,38 +68,39 @@ class LoginPacketHandler extends PacketHandler{
return true; return true;
} }
if(!Player::isValidUserName($packet->extraData[LoginPacket::I_USERNAME])){ if(!Player::isValidUserName($packet->extraData->displayName)){
$this->session->disconnect("disconnectionScreen.invalidName"); $this->session->disconnect("disconnectionScreen.invalidName");
return true; return true;
} }
try{ try{
$clientData = $packet->clientData; //this serves no purpose except readability
/** @var SkinAnimation[] $animations */ /** @var SkinAnimation[] $animations */
$animations = []; $animations = [];
foreach($packet->clientData[LoginPacket::I_ANIMATION_IMAGES] as $animation){ foreach($clientData->AnimatedImageData as $animation){
$animations[] = new SkinAnimation( $animations[] = new SkinAnimation(
new SkinImage( new SkinImage(
$animation[LoginPacket::I_ANIMATION_IMAGE_HEIGHT], $animation->ImageHeight,
$animation[LoginPacket::I_ANIMATION_IMAGE_WIDTH], $animation->ImageWidth,
base64_decode($animation[LoginPacket::I_ANIMATION_IMAGE_DATA], true) base64_decode($animation->Image, true)
), ),
$animation[LoginPacket::I_ANIMATION_IMAGE_TYPE], $animation->Type,
$animation[LoginPacket::I_ANIMATION_IMAGE_FRAMES] $animation->Frames
); );
} }
$skinData = new SkinData( $skinData = new SkinData(
$packet->clientData[LoginPacket::I_SKIN_ID], $clientData->SkinId,
base64_decode($packet->clientData[LoginPacket::I_SKIN_RESOURCE_PATCH], true), base64_decode($clientData->SkinResourcePatch, true),
new SkinImage($packet->clientData[LoginPacket::I_SKIN_HEIGHT], $packet->clientData[LoginPacket::I_SKIN_WIDTH], base64_decode($packet->clientData[LoginPacket::I_SKIN_DATA], true)), new SkinImage($clientData->SkinImageHeight, $clientData->SkinImageWidth, base64_decode($clientData->SkinData, true)),
$animations, $animations,
new SkinImage($packet->clientData[LoginPacket::I_CAPE_HEIGHT], $packet->clientData[LoginPacket::I_CAPE_WIDTH], base64_decode($packet->clientData[LoginPacket::I_CAPE_DATA], true)), new SkinImage($clientData->CapeImageHeight, $clientData->CapeImageWidth, base64_decode($clientData->CapeData, true)),
base64_decode($packet->clientData[LoginPacket::I_GEOMETRY_DATA], true), base64_decode($clientData->SkinGeometryData, true),
base64_decode($packet->clientData[LoginPacket::I_ANIMATION_DATA], true), base64_decode($clientData->SkinAnimationData, true),
$packet->clientData[LoginPacket::I_PREMIUM_SKIN], $clientData->PremiumSkin,
$packet->clientData[LoginPacket::I_PERSONA_SKIN], $clientData->PersonaSkin,
$packet->clientData[LoginPacket::I_PERSONA_CAPE_ON_CLASSIC_SKIN], $clientData->CapeOnClassicSkin,
$packet->clientData[LoginPacket::I_CAPE_ID] $clientData->CapeId
); );
$skin = SkinAdapterSingleton::get()->fromSkinData($skinData); $skin = SkinAdapterSingleton::get()->fromSkinData($skinData);
@ -111,12 +112,12 @@ class LoginPacketHandler extends PacketHandler{
} }
$this->session->setPlayerInfo(new PlayerInfo( $this->session->setPlayerInfo(new PlayerInfo(
$packet->extraData[LoginPacket::I_USERNAME], $packet->extraData->displayName,
UUID::fromString($packet->extraData[LoginPacket::I_UUID]), UUID::fromString($packet->extraData->identity),
$skin, $skin,
$packet->clientData[LoginPacket::I_LANGUAGE_CODE], $packet->clientData->LanguageCode,
$packet->extraData[LoginPacket::I_XUID], $packet->extraData->XUID,
$packet->clientData (array) $packet->clientData
)); ));
$ev = new PlayerPreLoginEvent( $ev = new PlayerPreLoginEvent(

View File

@ -25,77 +25,34 @@ namespace pocketmine\network\mcpe\protocol;
#include <rules/DataPacket.h> #include <rules/DataPacket.h>
use Particle\Validator\Validator;
use pocketmine\network\BadPacketException; use pocketmine\network\BadPacketException;
use pocketmine\network\mcpe\handler\PacketHandler; use pocketmine\network\mcpe\handler\PacketHandler;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\JwtChain;
use pocketmine\network\mcpe\serializer\NetworkBinaryStream; use pocketmine\network\mcpe\serializer\NetworkBinaryStream;
use pocketmine\utils\BinaryDataException; use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream; use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use function array_filter;
use function count;
use function implode;
use function is_array; use function is_array;
use function json_decode; use function json_decode;
use function json_last_error_msg;
class LoginPacket extends DataPacket implements ServerboundPacket{ class LoginPacket extends DataPacket implements ServerboundPacket{
public const NETWORK_ID = ProtocolInfo::LOGIN_PACKET; public const NETWORK_ID = ProtocolInfo::LOGIN_PACKET;
public const EDITION_POCKET = 0; public const EDITION_POCKET = 0;
public const I_USERNAME = 'displayName';
public const I_UUID = 'identity';
public const I_XUID = 'XUID';
public const I_CLIENT_RANDOM_ID = 'ClientRandomId';
public const I_SERVER_ADDRESS = 'ServerAddress';
public const I_LANGUAGE_CODE = 'LanguageCode';
public const I_SKIN_RESOURCE_PATCH = 'SkinResourcePatch';
public const I_SKIN_ID = 'SkinId';
public const I_SKIN_HEIGHT = 'SkinImageHeight';
public const I_SKIN_WIDTH = 'SkinImageWidth';
public const I_SKIN_DATA = 'SkinData';
public const I_CAPE_ID = 'CapeId';
public const I_CAPE_HEIGHT = 'CapeImageHeight';
public const I_CAPE_WIDTH = 'CapeImageWidth';
public const I_CAPE_DATA = 'CapeData';
public const I_GEOMETRY_DATA = 'SkinGeometryData';
public const I_ANIMATION_DATA = 'SkinAnimationData';
public const I_ANIMATION_IMAGES = 'AnimatedImageData';
public const I_ANIMATION_IMAGE_HEIGHT = 'ImageHeight';
public const I_ANIMATION_IMAGE_WIDTH = 'ImageWidth';
public const I_ANIMATION_IMAGE_FRAMES = 'Frames';
public const I_ANIMATION_IMAGE_TYPE = 'Type';
public const I_ANIMATION_IMAGE_DATA = 'Image';
public const I_PREMIUM_SKIN = 'PremiumSkin';
public const I_PERSONA_SKIN = 'PersonaSkin';
public const I_PERSONA_CAPE_ON_CLASSIC_SKIN = 'CapeOnClassicSkin';
/** @var int */ /** @var int */
public $protocol; public $protocol;
/** @var string[] array of encoded JWT */ /** @var JwtChain */
public $chainDataJwt = []; public $chainDataJwt;
/** /** @var AuthenticationData|null extraData index of whichever JWT has it */
* @var mixed[]|null extraData index of whichever JWT has it
* @phpstan-var array<string, mixed>
*/
public $extraData = null; public $extraData = null;
/** @var string */ /** @var string */
public $clientDataJwt; public $clientDataJwt;
/** /** @var ClientData decoded payload of the clientData JWT */
* @var mixed[] decoded payload of the clientData JWT public $clientData;
* @phpstan-var array<string, mixed>
*/
public $clientData = [];
/** /**
* This field may be used by plugins to bypass keychain verification. It should only be used for plugins such as * This field may be used by plugins to bypass keychain verification. It should only be used for plugins such as
@ -114,22 +71,6 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
$this->decodeConnectionRequest($in); $this->decodeConnectionRequest($in);
} }
/**
* @param mixed $data
*
* @throws BadPacketException
*/
private static function validate(Validator $v, string $name, $data) : void{
$result = $v->validate($data);
if($result->isNotValid()){
$messages = [];
foreach($result->getFailures() as $f){
$messages[] = $f->format();
}
throw new BadPacketException("Failed to validate '$name': " . implode(", ", $messages));
}
}
/** /**
* @throws BadPacketException * @throws BadPacketException
* @throws BinaryDataException * @throws BinaryDataException
@ -137,19 +78,19 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
protected function decodeConnectionRequest(NetworkBinaryStream $in) : void{ protected function decodeConnectionRequest(NetworkBinaryStream $in) : void{
$buffer = new BinaryStream($in->getString()); $buffer = new BinaryStream($in->getString());
$chainData = json_decode($buffer->get($buffer->getLInt()), true); $chainDataJson = json_decode($buffer->get($buffer->getLInt()));
if(!is_array($chainData)){ $mapper = new \JsonMapper;
throw new BadPacketException("Failed to decode chainData JSON: " . json_last_error_msg()); $mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
try{
$chainData = $mapper->map($chainDataJson, new JwtChain);
}catch(\JsonMapper_Exception $e){
throw BadPacketException::wrap($e);
} }
$vd = new Validator(); $this->chainDataJwt = $chainData;
$vd->required('chain')->isArray()->callback(function(array $data) : bool{
return count($data) <= 3 and count(array_filter($data, '\is_string')) === count($data);
});
self::validate($vd, "chainData", $chainData);
$this->chainDataJwt = $chainData['chain']; foreach($this->chainDataJwt->chain as $k => $chain){
foreach($this->chainDataJwt as $k => $chain){
//validate every chain element //validate every chain element
try{ try{
$claims = Utils::getJwtClaims($chain); $claims = Utils::getJwtClaims($chain);
@ -164,13 +105,15 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
throw new BadPacketException("Found 'extraData' more than once in chainData"); throw new BadPacketException("Found 'extraData' more than once in chainData");
} }
$extraV = new Validator(); $mapper = new \JsonMapper;
$extraV->required(self::I_USERNAME)->string(); $mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models
$extraV->required(self::I_UUID)->uuid(); $mapper->bExceptionOnMissingData = true;
$extraV->required(self::I_XUID)->string()->digits()->allowEmpty(true); $mapper->bExceptionOnUndefinedProperty = true;
self::validate($extraV, "chain.$k.extraData", $claims['extraData']); try{
$this->extraData = $mapper->map($claims['extraData'], new AuthenticationData);
$this->extraData = $claims['extraData']; }catch(\JsonMapper_Exception $e){
throw BadPacketException::wrap($e);
}
} }
} }
if($this->extraData === null){ if($this->extraData === null){
@ -184,40 +127,15 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
throw new BadPacketException($e->getMessage(), 0, $e); throw new BadPacketException($e->getMessage(), 0, $e);
} }
$v = new Validator(); $mapper = new \JsonMapper;
$v->required(self::I_CLIENT_RANDOM_ID)->integer(); $mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models
$v->required(self::I_SERVER_ADDRESS)->string(); $mapper->bExceptionOnMissingData = true;
$v->required(self::I_LANGUAGE_CODE)->string(); $mapper->bExceptionOnUndefinedProperty = true;
try{
$v->required(self::I_SKIN_RESOURCE_PATCH)->string(); $this->clientData = $mapper->map($clientData, new ClientData);
}catch(\JsonMapper_Exception $e){
$v->required(self::I_SKIN_ID)->string(); throw BadPacketException::wrap($e);
$v->required(self::I_SKIN_DATA)->string(); }
$v->required(self::I_SKIN_HEIGHT)->integer(true);
$v->required(self::I_SKIN_WIDTH)->integer(true);
$v->required(self::I_CAPE_ID, null, true)->string();
$v->required(self::I_CAPE_DATA, null, true)->string();
$v->required(self::I_CAPE_HEIGHT)->integer(true);
$v->required(self::I_CAPE_WIDTH)->integer(true);
$v->required(self::I_GEOMETRY_DATA, null, true)->string();
$v->required(self::I_ANIMATION_DATA, null, true)->string();
$v->required(self::I_ANIMATION_IMAGES, null, true)->isArray()->each(function(Validator $vSub) : void{
$vSub->required(self::I_ANIMATION_IMAGE_HEIGHT)->integer(true);
$vSub->required(self::I_ANIMATION_IMAGE_WIDTH)->integer(true);
$vSub->required(self::I_ANIMATION_IMAGE_FRAMES)->numeric(); //float() doesn't accept ints ???
$vSub->required(self::I_ANIMATION_IMAGE_TYPE)->integer(true);
$vSub->required(self::I_ANIMATION_IMAGE_DATA)->string();
});
$v->required(self::I_PREMIUM_SKIN)->bool();
$v->required(self::I_PERSONA_SKIN)->bool();
$v->required(self::I_PERSONA_CAPE_ON_CLASSIC_SKIN)->bool();
self::validate($v, 'clientData', $clientData);
$this->clientData = $clientData;
} }
protected function encodePayload(NetworkBinaryStream $out) : void{ protected function encodePayload(NetworkBinaryStream $out) : void{

View File

@ -0,0 +1,51 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol\types\login;
/**
* Model class for LoginPacket JSON data for JsonMapper
*/
final class AuthenticationData{
/**
* @var string
* @required
*/
public $displayName;
/**
* @var string
* @required
*/
public $identity;
/** @var string */
public $titleId = ""; //TODO: find out what this is for
/**
* @var string
* @required
*/
public $XUID;
}

View File

@ -0,0 +1,219 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol\types\login;
/**
* Model class for LoginPacket JSON data for JsonMapper
*/
final class ClientData{
/**
* @var ClientDataAnimationFrame[]
* @required
*/
public $AnimatedImageData;
/**
* @var string
* @required
*/
public $CapeData;
/**
* @var string
* @required
*/
public $CapeId;
/**
* @var int
* @required
*/
public $CapeImageHeight;
/**
* @var int
* @required
*/
public $CapeImageWidth;
/**
* @var bool
* @required
*/
public $CapeOnClassicSkin;
/**
* @var int
* @required
*/
public $ClientRandomId;
/**
* @var int
* @required
*/
public $CurrentInputMode;
/**
* @var int
* @required
*/
public $DefaultInputMode;
/**
* @var string
* @required
*/
public $DeviceId;
/**
* @var string
* @required
*/
public $DeviceModel;
/**
* @var int
* @required
*/
public $DeviceOS;
/**
* @var string
* @required
*/
public $GameVersion;
/**
* @var int
* @required
*/
public $GuiScale;
/**
* @var string
* @required
*/
public $LanguageCode;
/**
* @var bool
* @required
*/
public $PersonaSkin;
/**
* @var string
* @required
*/
public $PlatformOfflineId;
/**
* @var string
* @required
*/
public $PlatformOnlineId;
/** @var string */
public $PlatformUserId = ""; //xbox-only, apparently
/**
* @var bool
* @required
*/
public $PremiumSkin = false;
/**
* @var string
* @required
*/
public $SelfSignedId;
/**
* @var string
* @required
*/
public $ServerAddress;
/**
* @var string
* @required
*/
public $SkinAnimationData;
/**
* @var string
* @required
*/
public $SkinData;
/**
* @var string
* @required
*/
public $SkinGeometryData;
/**
* @var string
* @required
*/
public $SkinId;
/**
* @var int
* @required
*/
public $SkinImageHeight;
/**
* @var int
* @required
*/
public $SkinImageWidth;
/**
* @var string
* @required
*/
public $SkinResourcePatch;
/**
* @var string
* @required
*/
public $ThirdPartyName;
/**
* @var bool
* @required
*/
public $ThirdPartyNameOnly;
/**
* @var int
* @required
*/
public $UIProfile;
}

View File

@ -0,0 +1,60 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol\types\login;
/**
* Model class for LoginPacket JSON data for JsonMapper
*/
final class ClientDataAnimationFrame{
/**
* @var int
* @required
*/
public $ImageHeight;
/**
* @var int
* @required
*/
public $ImageWidth;
/**
* @var float
* @required
*/
public $Frames;
/**
* @var int
* @required
*/
public $Type;
/**
* @var string
* @required
*/
public $Image;
}

View File

@ -0,0 +1,36 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol\types\login;
/**
* Model class for LoginPacket JSON data for JsonMapper
*/
final class JwtChain{
/**
* @var string[]
* @required
*/
public $chain;
}

View File

@ -0,0 +1,19 @@
<?php
//possible bug in phpstan requires this to be defined here
class JsonMapper_Exception extends \Exception{}
class JsonMapper{
/**
* @template TModel of object
*
* @param mixed[]|object $json
* @param TModel $object
*
* @return TModel
*
* @throws JsonMapper_Exception
*/
public function map($json, object $object) : object{}
}