mirror of
https://github.com/danog/MadelineProto.git
synced 2024-11-27 00:34:40 +01:00
560 lines
21 KiB
PHP
560 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* This file is part of MadelineProto.
|
|
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
* See the GNU Affero General Public License for more details.
|
|
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* @author Daniil Gentili <daniil@daniil.it>
|
|
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
|
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
|
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
|
*/
|
|
|
|
namespace danog\MadelineProto\TL;
|
|
|
|
use AssertionError;
|
|
use danog\MadelineProto\Connection;
|
|
use danog\MadelineProto\Lang;
|
|
use danog\MadelineProto\MTProto;
|
|
use danog\MadelineProto\MTProtoTools\MinDatabase;
|
|
use danog\MadelineProto\MTProtoTools\PeerDatabase;
|
|
use danog\MadelineProto\MTProtoTools\ReferenceDatabase;
|
|
use danog\MadelineProto\SecurityException;
|
|
use danog\MadelineProto\Settings\TLSchema;
|
|
use ReflectionClass;
|
|
use ReflectionFunction;
|
|
use Webmozart\Assert\Assert;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class Builder
|
|
{
|
|
/**
|
|
* TL instance.
|
|
*/
|
|
protected TL $TL;
|
|
protected readonly array $byType;
|
|
protected readonly array $idByPredicate;
|
|
protected readonly array $typeByPredicate;
|
|
protected readonly array $constructorByPredicate;
|
|
protected readonly array $methodVectorTypes;
|
|
protected readonly string $class;
|
|
protected $output;
|
|
public function __construct(
|
|
TLSchema $settings,
|
|
/**
|
|
* Output file.
|
|
*/
|
|
string $output,
|
|
/**
|
|
* Output namespace.
|
|
*/
|
|
protected string $namespace,
|
|
) {
|
|
$this->output = fopen($output, 'w');
|
|
$this->TL = new TL();
|
|
$this->TL->init($settings);
|
|
|
|
$this->class = basename($output, '.php');
|
|
|
|
$callbacks = [];
|
|
$callbacks []= (new ReflectionClass(MTProto::class))->newInstanceWithoutConstructor();
|
|
$callbacks []= (new ReflectionClass(ReferenceDatabase::class))->newInstanceWithoutConstructor();
|
|
$callbacks []= (new ReflectionClass(MinDatabase::class))->newInstanceWithoutConstructor();
|
|
$callbacks []= (new ReflectionClass(PeerDatabase::class))->newInstanceWithoutConstructor();
|
|
$this->TL->updateCallbacks($callbacks);
|
|
|
|
$byType = [];
|
|
$idByPredicate = ['vector' => var_export(hex2bin('1cb5c415'), true)];
|
|
$constructorByPredicate = [];
|
|
foreach ($this->TL->getConstructors()->by_id as $id => $constructor) {
|
|
foreach ($constructor['params'] as &$param) {
|
|
if ($constructor['predicate'] === 'photoStrippedSize'
|
|
&& $param['name'] === 'bytes'
|
|
) {
|
|
$param['name'] = 'inflated';
|
|
$param['type'] = 'inflated';
|
|
}
|
|
|
|
if ($param['name'] === 'random_bytes') {
|
|
$param['type'] = 'random_bytes';
|
|
}
|
|
|
|
if (isset($param['subtype'])) {
|
|
$param['type'] = match ($param['type']) {
|
|
'Vector t' => "Vector<{$param['subtype']}>",
|
|
'vector' => "vector<{$param['subtype']}>",
|
|
};
|
|
}
|
|
}
|
|
|
|
if (isset($constructor['layer'])) {
|
|
$constructor['predicate'] .= '_'.$constructor['layer'];
|
|
if (!$this instanceof SecretBuilder) {
|
|
continue;
|
|
}
|
|
}
|
|
$constructor['id'] = $id;
|
|
|
|
$constructorByPredicate[$constructor['predicate']] = $constructor;
|
|
$idByPredicate[$constructor['predicate']] = var_export($id, true);
|
|
$typeByPredicate[$constructor['predicate']] = $constructor['type'];
|
|
$byType[$constructor['type']][$id]= $constructor;
|
|
}
|
|
$this->idByPredicate = $idByPredicate;
|
|
$this->typeByPredicate = $typeByPredicate;
|
|
$this->constructorByPredicate = $constructorByPredicate;
|
|
|
|
$methodConstructors = [];
|
|
$methodVectorTypes = [];
|
|
foreach ($this->TL->getMethods()->by_id as $c) {
|
|
['type' => $type, 'method' => $name, 'encrypted' => $encrypted] = $c;
|
|
if (!$encrypted) {
|
|
continue;
|
|
}
|
|
// TODO
|
|
if ($type === 'X') {
|
|
continue;
|
|
}
|
|
if (isset($c['subtype'])) {
|
|
$methodVectorTypes[$name] = match ($c['type']) {
|
|
'Vector t' => "Vector<{$c['subtype']}>",
|
|
'vector' => "vector<{$c['subtype']}>",
|
|
};
|
|
continue;
|
|
}
|
|
$methodConstructors = array_merge($methodConstructors, $byType[$type]);
|
|
}
|
|
|
|
$byType['MethodResult'] = $methodConstructors;
|
|
$this->methodVectorTypes = $methodVectorTypes;
|
|
$this->byType = $byType;
|
|
}
|
|
|
|
protected static function escapeConstructorName(array $constructor): string
|
|
{
|
|
return str_replace(['.', ' '], '___', $constructor['predicate']);
|
|
}
|
|
protected static function escapeTypeName(string $name): string
|
|
{
|
|
return str_replace(['.', ' '], '___', $name);
|
|
}
|
|
protected function needFullConstructor(string $predicate): bool
|
|
{
|
|
if (isset($this->TL->beforeConstructorDeserialization[$predicate])
|
|
|| isset($this->TL->afterConstructorDeserialization[$predicate])) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
protected static function methodFromClosure(\Closure $closure): string
|
|
{
|
|
$refl = new ReflectionFunction($closure);
|
|
return match ($refl->getClosureThis()::class) {
|
|
PeerDatabase::class => '$this->peerDatabase',
|
|
MinDatabase::class => '$this->minDatabase?',
|
|
ReferenceDatabase::class => '$this->referenceDatabase?',
|
|
MTProto::class => '$this->API',
|
|
}."->".$refl->getName();
|
|
}
|
|
|
|
protected function buildTypes(array $constructors, string $type): string
|
|
{
|
|
$typeMethod = "_type_".self::escapeTypeName($type);
|
|
$result = "match (stream_get_contents(\$stream, 4)) {\n";
|
|
foreach ($constructors as ['predicate' => $predicate]) {
|
|
if ($predicate === 'gzip_packed') {
|
|
continue;
|
|
}
|
|
if ($predicate === 'jsonObjectValue') {
|
|
throw new AssertionError("Impossible!");
|
|
}
|
|
$result .= $this->idByPredicate[$predicate]." => ";
|
|
$result .= $this->buildConstructor($predicate);
|
|
$result .= ",\n";
|
|
}
|
|
if ($type === 'MethodResult') {
|
|
$result .= $this->idByPredicate['gzip_packed']." => ".$this->methodCall(
|
|
"deserialize$typeMethod",
|
|
'self::gzdecode($stream), $method'
|
|
).",\n";
|
|
$result .= $this->idByPredicate['vector']." => match (\$method) {\n";
|
|
foreach ($this->methodVectorTypes as $method => $type) {
|
|
$result .= var_export($method, true) . ' => '.$this->buildType($type).",\n";
|
|
}
|
|
$result .= "},\n";
|
|
} else {
|
|
$result .= $this->idByPredicate['gzip_packed']." => ".$this->methodCall(
|
|
"deserialize$typeMethod",
|
|
'self::gzdecode($stream)'
|
|
).",\n";
|
|
}
|
|
$result .= "default => self::err(\$stream)\n";
|
|
return $result."}\n";
|
|
}
|
|
protected array $createdConstructors = [];
|
|
public function buildConstructor(string $predicate): string
|
|
{
|
|
$constructor = $this->constructorByPredicate[$predicate];
|
|
Assert::notFalse($constructor, "Missing constructor $predicate");
|
|
[
|
|
'flags' => $flags,
|
|
'params' => $params,
|
|
] = $constructor;
|
|
|
|
if ($predicate === 'rpc_result') {
|
|
$result = "\$tmp = ['_' => '$predicate', 'req_msg_id' => \$id = {$this->buildType('long')}];\n";
|
|
$result .= '$message = $this->connection->outgoing_messages[$id];
|
|
$method = $message->constructor;
|
|
if (isset($this->beforeMethodResponseDeserialization[$method])) {
|
|
foreach ($this->beforeMethodResponseDeserialization[$method] as $callback) {
|
|
$callback($method);
|
|
}
|
|
}
|
|
$tmp["result"] = '.$this->buildType('MethodResult').';
|
|
if (isset($this->afterMethodResponseDeserialization[$method])) {
|
|
foreach ($this->afterMethodResponseDeserialization[$method] as $callback) {
|
|
$callback($tmp);
|
|
}
|
|
}
|
|
';
|
|
} elseif ($flags) {
|
|
$result = $this->buildConstructorFull($predicate, $params, $flags);
|
|
} else {
|
|
$result = $this->buildConstructorShort($predicate, $params, $flags);
|
|
if (!$this->needFullConstructor($predicate)) {
|
|
return $result;
|
|
}
|
|
$result = "\$tmp = $result;\n";
|
|
}
|
|
|
|
$pre = '';
|
|
foreach ($this->TL->beforeConstructorDeserialization[$predicate] ?? [] as $closure) {
|
|
$pre .= self::methodFromClosure($closure)."('$predicate');\n";
|
|
}
|
|
$result = $pre.$result;
|
|
foreach ($this->TL->afterConstructorDeserialization[$predicate] ?? [] as $closure) {
|
|
$result .= self::methodFromClosure($closure)."(\$tmp);\n";
|
|
}
|
|
$result .= "return \$tmp;\n";
|
|
|
|
$nameEscaped = self::escapeConstructorName($constructor);
|
|
if (!isset($this->createdConstructors[$predicate])) {
|
|
$this->createdConstructors[$predicate] = true;
|
|
$this->m("deserialize_$nameEscaped", $result);
|
|
}
|
|
|
|
return $this->methodCall("deserialize_$nameEscaped");
|
|
}
|
|
protected function buildConstructorFull(string $predicate, array $params, array $flags): string
|
|
{
|
|
$result = "\$tmp = ['_' => '$predicate'];\n";
|
|
$flagNames = [];
|
|
foreach ($flags as ['flag' => $flag]) {
|
|
$flagNames[$flag] = true;
|
|
}
|
|
foreach ($params as $param) {
|
|
$name = $param['name'];
|
|
if (!isset($param['pow'])) {
|
|
$code = $this->buildType($param['type']);
|
|
|
|
if (isset($flagNames[$name])) {
|
|
$result .= "\$$name = $code;\n";
|
|
} else {
|
|
$result .= "\$tmp['$name'] = $code;\n";
|
|
}
|
|
continue;
|
|
}
|
|
$flag = "(\${$param['flag']} & {$param['pow']}) !== 0";
|
|
if ($param['type'] === 'true') {
|
|
$result .= "\$tmp['$name'] = $flag;\n";
|
|
continue;
|
|
}
|
|
$code = $this->buildType($param['type']);
|
|
$result .= "if ($flag) \$tmp['$name'] = $code;\n";
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
protected function buildConstructorShort(string $predicate, array $params = []): string
|
|
{
|
|
if ($predicate === 'dataJSON') {
|
|
return 'json_decode('.$this->buildType('string').', true, 512, \\JSON_THROW_ON_ERROR)';
|
|
}
|
|
if ($predicate === 'jsonNull') {
|
|
return 'null';
|
|
}
|
|
$superBare = $this->typeByPredicate[$predicate] === 'JSONValue'
|
|
|| $this->typeByPredicate[$predicate] === 'Peer';
|
|
|
|
if ($superBare) {
|
|
$result = $this->buildType(end($params)['type']);
|
|
} else {
|
|
$result = "[\n";
|
|
$result .= "'_' => '$predicate',\n";
|
|
foreach ($params as $param) {
|
|
$result .= var_export($param['name'], true).' => ';
|
|
$result .= $this->buildType($param['type']).",\n";
|
|
}
|
|
$result .= ']';
|
|
}
|
|
|
|
if ($predicate === 'peerChat') {
|
|
$result = "-$result";
|
|
} elseif ($predicate === 'peerChannel') {
|
|
$result = "-1000000000000 - $result";
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
protected array $createdVectors = [];
|
|
protected function buildVector(string $type, bool $bare, ?string $payload = null): string
|
|
{
|
|
if (!isset($this->createdVectors[$type])) {
|
|
$this->createdVectors[$type] = true;
|
|
if ($type === 'JSONObjectValue') {
|
|
$payload = '$result['.$this->buildType('string').'] = '.$this->buildType('JSONValue');
|
|
} elseif (isset($this->byType[$type])) {
|
|
$payload = '$result []= '.$this->buildTypes($this->byType[$type], $type);
|
|
} elseif ($payload === null) {
|
|
if ($type === '%MTMessage') {
|
|
$type = 'MTmessage';
|
|
}
|
|
$payload = '$result []= '.$this->buildConstructor($type);
|
|
}
|
|
$this->m("deserialize_type_array_of_{$this->escapeTypeName($type)}", '
|
|
$result = [];
|
|
for ($x = unpack("V", stream_get_contents($stream, 4))[1]; $x > 0; --$x) {
|
|
'.$payload.';
|
|
}
|
|
return $result;
|
|
', 'array', static: $type === 'JSONValue');
|
|
}
|
|
return $this->methodCall(
|
|
"deserialize_type_array_of_{$this->escapeTypeName($type)}",
|
|
$bare ? '$stream' : 'match(stream_get_contents($stream, 4)) {
|
|
'.$this->idByPredicate['vector'].' => $stream,
|
|
'.$this->idByPredicate['gzip_packed'].' => self::gzdecode_vector($stream)
|
|
}'
|
|
);
|
|
}
|
|
|
|
protected array $createdTypes = ['Object' => true];
|
|
protected array $typeStack = [];
|
|
protected function buildType(string $type): string
|
|
{
|
|
if (str_starts_with($type, 'Vector<')) {
|
|
return $this->buildVector(str_replace(['Vector<', '>'], '', $type), false);
|
|
}
|
|
if (str_starts_with($type, 'vector<')) {
|
|
return $this->buildVector(str_replace(['vector<', '>'], '', $type), true);
|
|
}
|
|
$tmp = match ($type) {
|
|
'#' => "unpack('V', stream_get_contents(\$stream, 4))[1]",
|
|
'int' => "unpack('l', stream_get_contents(\$stream, 4))[1]",
|
|
'long' => "unpack('q', stream_get_contents(\$stream, 8))[1]",
|
|
'double' => "unpack('d', stream_get_contents(\$stream, 8))[1]",
|
|
'Bool' => 'match (stream_get_contents($stream, 4)) {'.
|
|
$this->idByPredicate['boolTrue'].' => true,'.
|
|
$this->idByPredicate['boolFalse'].' => false, default => '.$this->methodCall('err').' }',
|
|
'strlong' => 'stream_get_contents($stream, 8)',
|
|
'int128' => 'stream_get_contents($stream, 16)',
|
|
'int256' => 'stream_get_contents($stream, 32)',
|
|
'int512' => 'stream_get_contents($stream, 64)',
|
|
'string', 'bytes', 'waveform', 'random_bytes' =>
|
|
$this->methodCall("deserialize_$type"),
|
|
'inflated' =>
|
|
'new Types\Bytes(Tools::inflateStripped('.$this->methodCall("deserialize_string").'))',
|
|
default => null
|
|
};
|
|
if ($tmp !== null) {
|
|
return $tmp;
|
|
}
|
|
|
|
if (!isset($this->createdTypes[$type])) {
|
|
$this->createdTypes[$type] = true;
|
|
|
|
$this->m(
|
|
"deserialize_type_{$this->escapeTypeName($type)}",
|
|
"return {$this->buildTypes($this->byType[$type], $type)};",
|
|
static: $type === 'JSONValue'
|
|
);
|
|
}
|
|
|
|
$had = array_search($type, $this->typeStack, true) !== false;
|
|
$this->typeStack []= $type;
|
|
try {
|
|
if (!$had) {
|
|
return $this->buildTypes($this->byType[$type], $type);
|
|
}
|
|
return $this->methodCall("deserialize_type_{$this->escapeTypeName($type)}");
|
|
} finally {
|
|
array_pop($this->typeStack);
|
|
}
|
|
}
|
|
|
|
protected array $methodsCreated = [];
|
|
protected function methodCall(string $method, string $stream = '$stream'): string
|
|
{
|
|
return ($this->methodsCreated[$method] ?? true)
|
|
? "\$this->$method($stream)"
|
|
: "self::$method($stream)";
|
|
}
|
|
|
|
public function m(string $methodName, string $body, string $returnType = 'mixed', bool $public = false, bool $static = false, string $extraArg = ''): void
|
|
{
|
|
if (isset($this->methodsCreated[$methodName])) {
|
|
throw new AssertionError("Already created $methodName!");
|
|
}
|
|
|
|
$this->methodsCreated[$methodName] = $static;
|
|
$public = $public ? 'public' : 'private';
|
|
$static = $static ? 'static' : '';
|
|
$this->w(" $public $static function $methodName(mixed \$stream$extraArg): $returnType {\n{$body}\n }\n");
|
|
}
|
|
protected function w(string $data): void
|
|
{
|
|
fwrite($this->output, $data);
|
|
}
|
|
public function build(): void
|
|
{
|
|
$this->w("<?php namespace {$this->namespace};\n/** @internal Autogenerated using tools/TL/Builder.php */\n");
|
|
foreach ([
|
|
MTProto::class,
|
|
Connection::class,
|
|
PeerDatabase::class,
|
|
ReferenceDatabase::class,
|
|
MinDatabase::class,
|
|
SecurityException::class,
|
|
AssertionError::class,
|
|
Lang::class,
|
|
] as $clazz) {
|
|
$this->w("use {$clazz};\n");
|
|
}
|
|
|
|
$this->w("final class {$this->class} {\n");
|
|
|
|
$this->w('public function __construct(
|
|
private readonly MTProto $API,
|
|
private readonly Connection $connection,
|
|
private readonly PeerDatabase $peerDatabase,
|
|
private readonly ?ReferenceDatabase $referenceDatabase,
|
|
private readonly ?MinDatabase $minDatabase,
|
|
) {}
|
|
');
|
|
|
|
$this->m('err', '
|
|
fseek($stream, -4, SEEK_CUR);
|
|
throw new AssertionError("Unexpected ID ".bin2hex(fread($stream, 4)));
|
|
', 'never');
|
|
|
|
$this->m("gzdecode", "
|
|
\$res = fopen('php://memory', 'rw+b');
|
|
fwrite(\$res, gzdecode(self::deserialize_string(\$stream)));
|
|
rewind(\$res);
|
|
return \$res;
|
|
");
|
|
|
|
$this->m('gzdecode_vector', "
|
|
\$res = fopen('php://memory', 'rw+b');
|
|
fwrite(\$res, gzdecode(self::deserialize_string(\$stream)));
|
|
rewind(\$res);
|
|
return match (stream_get_contents(\$stream, 4)) {
|
|
{$this->idByPredicate['vector']} => \$stream,
|
|
default => self::err(\$stream)
|
|
};
|
|
");
|
|
|
|
$block_str = '
|
|
$l = \ord(stream_get_contents($stream, 1));
|
|
if ($l > 254) {
|
|
throw new Exception(Lang::$current_lang["length_too_big"]);
|
|
}
|
|
if ($l === 254) {
|
|
$l = unpack("V", stream_get_contents($stream, 3).\chr(0))[1];
|
|
$x = stream_get_contents($stream, $l);
|
|
$resto = (-$l) % 4;
|
|
$resto = $resto < 0 ? $resto + 4 : $resto;
|
|
if ($resto > 0) {
|
|
stream_get_contents($stream, $resto);
|
|
}
|
|
} else {
|
|
$x = $l ? stream_get_contents($stream, $l) : "";
|
|
$resto = (-$l+1) % 4;
|
|
$resto = $resto < 0 ? $resto + 4 : $resto;
|
|
if ($resto > 0) {
|
|
stream_get_contents($stream, $resto);
|
|
}
|
|
}'."\n";
|
|
|
|
$this->m("deserialize_bytes", "
|
|
$block_str
|
|
return new Types\Bytes(\$x);
|
|
");
|
|
$this->m("deserialize_string", "
|
|
$block_str
|
|
return \$x;
|
|
");
|
|
$this->m("deserialize_waveform", "
|
|
$block_str
|
|
return TL::extractWaveform(\$x);
|
|
");
|
|
|
|
$this->m('deserialize_random_bytes', '
|
|
$l = \ord(stream_get_contents($stream, 1));
|
|
if ($l > 254) {
|
|
throw new Exception(Lang::$current_lang["length_too_big"]);
|
|
}
|
|
if ($l === 254) {
|
|
$l = unpack("V", stream_get_contents($stream, 3).\chr(0))[1];
|
|
if ($l < 15) {
|
|
throw new SecurityException("Random_bytes is too small!");
|
|
}
|
|
} else {
|
|
if ($l < 15) {
|
|
throw new SecurityException("Random_bytes is too small!");
|
|
}
|
|
$l += 1;
|
|
}
|
|
$resto = (-$l) % 4;
|
|
$resto = $resto < 0 ? $resto + 4 : $resto;
|
|
if ($resto > 0) {
|
|
$l += $resto;
|
|
}
|
|
stream_get_contents($stream, $l);
|
|
', 'void');
|
|
|
|
foreach (['int', 'long', 'double', 'strlong', 'string', 'bytes'] as $type) {
|
|
$this->buildVector($type, false, '$result []= '.$this->buildType($type));
|
|
}
|
|
|
|
$this->buildMain();
|
|
|
|
$this->w("}\n");
|
|
}
|
|
|
|
protected function buildMain(): void
|
|
{
|
|
$initial_constructors = array_filter(
|
|
$this->constructorByPredicate,
|
|
static fn (array $arr) => (
|
|
$arr['type'] === 'Update'
|
|
|| $arr['predicate'] === 'rpc_result'
|
|
|| !$arr['encrypted']
|
|
) && (
|
|
$arr['predicate'] !== 'rpc_error'
|
|
&& $arr['predicate'] !== 'MTmessage'
|
|
)
|
|
);
|
|
|
|
$this->m("deserialize_type_Object", "return {$this->buildTypes($initial_constructors, 'Object')};", 'mixed', true, static: false);
|
|
}
|
|
}
|