This commit is contained in:
Daniil Gentili 2024-04-01 17:51:11 +02:00
parent 3c2194f2a5
commit 8481b5c66b
5 changed files with 271 additions and 272 deletions

View File

@ -22,10 +22,7 @@
"autoload": {
"psr-4": {
"danog\\Decoder\\": "src/"
},
"files": [
"src/functions.php"
]
}
},
"autoload-dev": {
"psr-4": {

View File

@ -111,7 +111,7 @@ final class FileId
public static function fromBotAPI(string $fileId): self
{
$orig = $fileId;
$fileId = rleDecode(base64urlDecode($fileId));
$fileId = Tools::rleDecode(Tools::base64urlDecode($fileId));
$version = \ord($fileId[\strlen($fileId) - 1]);
$subVersion = $version === 4 ? \ord($fileId[\strlen($fileId) - 2]) : 0;
@ -126,16 +126,16 @@ final class FileId
return $res;
};
$typeId = unpackInt($read(4));
$dc_id = unpackInt($read(4));
$fileReference = $typeId & FILE_REFERENCE_FLAG ? readTLString($fileId) : null;
$hasWebLocation = (bool) ($typeId & WEB_LOCATION_FLAG);
$typeId &= ~FILE_REFERENCE_FLAG;
$typeId &= ~WEB_LOCATION_FLAG;
$typeId = Tools::unpackInt($read(4));
$dc_id = Tools::unpackInt($read(4));
$fileReference = $typeId & Tools::FILE_REFERENCE_FLAG ? Tools::readTLString($fileId) : null;
$hasWebLocation = (bool) ($typeId & Tools::WEB_LOCATION_FLAG);
$typeId &= ~Tools::FILE_REFERENCE_FLAG;
$typeId &= ~Tools::WEB_LOCATION_FLAG;
if ($hasWebLocation) {
$url = readTLString($fileId);
$access_hash = unpackLong($read(8));
$url = Tools::readTLString($fileId);
$access_hash = Tools::unpackLong($read(8));
return new self(
$dc_id,
FileIdType::from($typeId),
@ -147,28 +147,28 @@ final class FileId
subVersion: $subVersion
);
}
$id = unpackLong($read(8));
$access_hash = unpackLong($read(8));
$id = Tools::unpackLong($read(8));
$access_hash = Tools::unpackLong($read(8));
$volume_id = null;
$local_id = null;
$photoSizeSource = null;
if ($typeId <= FileIdType::PHOTO->value) {
if ($subVersion < 32) {
$volume_id = unpackLong($read(8));
$local_id = unpackInt($read(4));
$volume_id = Tools::unpackLong($read(8));
$local_id = Tools::unpackInt($read(4));
}
/** @psalm-suppress MixedArgument */
$photosize_source = PhotoSizeSourceType::from($subVersion >= 4 ? \unpack('V', $read(4))[1] : 0);
switch ($photosize_source) {
case PhotoSizeSourceType::LEGACY:
$photoSizeSource = new PhotoSizeSourceLegacy(unpackLong($read(8)));
$photoSizeSource = new PhotoSizeSourceLegacy(Tools::unpackLong($read(8)));
break;
case PhotoSizeSourceType::FULL_LEGACY:
$volume_id = unpackLong($read(8));
$photoSizeSource = new PhotoSizeSourceLegacy(unpackLong($read(8)));
$local_id = unpackInt($read(4));
$volume_id = Tools::unpackLong($read(8));
$photoSizeSource = new PhotoSizeSourceLegacy(Tools::unpackLong($read(8)));
$local_id = Tools::unpackInt($read(4));
break;
case PhotoSizeSourceType::THUMBNAIL:
/** @var array{file_type: int, thumbnail_type: string} */
@ -184,14 +184,14 @@ final class FileId
? PhotoSizeSourceDialogPhotoSmall::class
: PhotoSizeSourceDialogPhotoBig::class;
$photoSizeSource = new $clazz(
unpackLong($read(8)),
unpackLong($read(8)),
Tools::unpackLong($read(8)),
Tools::unpackLong($read(8)),
);
break;
case PhotoSizeSourceType::STICKERSET_THUMBNAIL:
$photoSizeSource = new PhotoSizeSourceStickersetThumbnail(
unpackLong($read(8)),
unpackLong($read(8))
Tools::unpackLong($read(8)),
Tools::unpackLong($read(8))
);
break;
case PhotoSizeSourceType::DIALOGPHOTO_BIG_LEGACY:
@ -200,27 +200,27 @@ final class FileId
? PhotoSizeSourceDialogPhotoSmall::class
: PhotoSizeSourceDialogPhotoBig::class;
$photoSizeSource = new $clazz(
unpackLong($read(8)),
unpackLong($read(8))
Tools::unpackLong($read(8)),
Tools::unpackLong($read(8))
);
$volume_id = unpackLong($read(8));
$local_id = unpackInt($read(4));
$volume_id = Tools::unpackLong($read(8));
$local_id = Tools::unpackInt($read(4));
break;
case PhotoSizeSourceType::STICKERSET_THUMBNAIL_LEGACY:
$photoSizeSource = new PhotoSizeSourceStickersetThumbnail(
unpackLong($read(8)),
unpackLong($read(8)),
Tools::unpackLong($read(8)),
Tools::unpackLong($read(8)),
);
$volume_id = unpackLong($read(8));
$local_id = unpackInt($read(4));
$volume_id = Tools::unpackLong($read(8));
$local_id = Tools::unpackInt($read(4));
break;
case PhotoSizeSourceType::STICKERSET_THUMBNAIL_VERSION:
$photoSizeSource = new PhotoSizeSourceStickersetThumbnailVersion(
unpackLong($read(8)),
unpackLong($read(8)),
unpackInt($read(4))
Tools::unpackLong($read(8)),
Tools::unpackLong($read(8)),
Tools::unpackInt($read(4))
);
break;
}
@ -253,25 +253,25 @@ final class FileId
{
$type = $this->type->value;
if ($this->fileReference !== null) {
$type |= FILE_REFERENCE_FLAG;
$type |= Tools::FILE_REFERENCE_FLAG;
}
if ($this->url !== null) {
$type |= WEB_LOCATION_FLAG;
$type |= Tools::WEB_LOCATION_FLAG;
}
$fileId = \pack('VV', $type, $this->dcId);
if ($this->fileReference !== null) {
$fileId .= packTLString($this->fileReference);
$fileId .= Tools::packTLString($this->fileReference);
}
if ($this->url !== null) {
$fileId .= packTLString($this->url);
$fileId .= packLong($this->accessHash);
return base64urlEncode(rleEncode($fileId));
$fileId .= Tools::packTLString($this->url);
$fileId .= Tools::packLong($this->accessHash);
return Tools::base64urlEncode(Tools::rleEncode($fileId));
}
\assert($this->id !== null);
$fileId .= packLong($this->id);
$fileId .= packLong($this->accessHash);
$fileId .= Tools::packLong($this->id);
$fileId .= Tools::packLong($this->accessHash);
if ($this->photoSizeSource !== null) {
$photoSize = $this->photoSizeSource;
@ -281,11 +281,11 @@ final class FileId
if ($this->volumeId === null) {
$writeExtra = true;
$fileId .= \pack('V', PhotoSizeSourceType::LEGACY->value);
$fileId .= packLong($photoSize->secret);
$fileId .= Tools::packLong($photoSize->secret);
} else {
$fileId .= \pack('V', PhotoSizeSourceType::FULL_LEGACY->value);
$fileId .= packLong($this->volumeId);
$fileId .= packLong($photoSize->secret);
$fileId .= Tools::packLong($this->volumeId);
$fileId .= Tools::packLong($photoSize->secret);
$fileId .= \pack('l', $this->localId);
}
break;
@ -307,22 +307,22 @@ final class FileId
: PhotoSizeSourceType::DIALOGPHOTO_BIG->value
)
);
$fileId .= packLong($photoSize->dialogId);
$fileId .= packLong($photoSize->dialogAccessHash);
$fileId .= Tools::packLong($photoSize->dialogId);
$fileId .= Tools::packLong($photoSize->dialogAccessHash);
break;
case $photoSize instanceof PhotoSizeSourceStickersetThumbnail:
$writeExtra = $this->volumeId !== null;
$fileId .= packLong($photoSize->stickerSetId);
$fileId .= packLong($photoSize->stickerSetAccessHash);
$fileId .= Tools::packLong($photoSize->stickerSetId);
$fileId .= Tools::packLong($photoSize->stickerSetAccessHash);
break;
case $photoSize instanceof PhotoSizeSourceStickersetThumbnailVersion:
$fileId .= packLong($photoSize->stickerSetId);
$fileId .= packLong($photoSize->stickerSetAccessHash);
$fileId .= Tools::packLong($photoSize->stickerSetId);
$fileId .= Tools::packLong($photoSize->stickerSetAccessHash);
$fileId .= \pack('l', $photoSize->stickerSetVersion);
break;
}
if ($writeExtra && $this->volumeId !== null && $this->localId !== null) {
$fileId .= packLong($this->volumeId);
$fileId .= Tools::packLong($this->volumeId);
$fileId .= \pack('l', $this->localId);
}
}
@ -332,7 +332,7 @@ final class FileId
}
$fileId .= \chr($this->version);
return base64urlEncode(rleEncode($fileId));
return Tools::base64urlEncode(Tools::rleEncode($fileId));
}
/**

205
src/Tools.php Normal file
View File

@ -0,0 +1,205 @@
<?php declare(strict_types=1);
namespace danog\Decoder;
\define('BIG_ENDIAN', \pack('L', 1) === \pack('N', 1));
final class Tools
{
public const WEB_LOCATION_FLAG = 1 << 24;
public const FILE_REFERENCE_FLAG = 1 << 25;
/**
* Unpack long properly, returns an actual number in any case.
*
* @internal
*
* @param string $field Field to unpack
*/
public static function unpackLong(string $field): int
{
/** @psalm-suppress MixedReturnStatement */
return \unpack('q', BIG_ENDIAN ? \strrev($field) : $field)[1];
}
/**
* Unpack integer.
* @internal
*
* @param string $field Field to unpack
*/
public static function unpackInt(string $field): int
{
/** @psalm-suppress MixedReturnStatement */
return \unpack('l', $field)[1];
}
/**
* Pack string long.
*
* @internal
*
*/
public static function packLong(int $field): string
{
$res = \pack('q', $field);
return BIG_ENDIAN ? \strrev($res) : $res;
}
/**
* Base64URL decode.
*
* @param string $data Data to decode
*
* @internal
*
*/
public static function base64urlDecode(string $data): string
{
return \base64_decode(\str_pad(\strtr($data, '-_', '+/'), \strlen($data) % 4, '=', STR_PAD_RIGHT));
}
/**
* Base64URL encode.
*
* @param string $data Data to encode
*
* @internal
*
*/
public static function base64urlEncode(string $data): string
{
return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '=');
}
/**
* Null-byte RLE decode.
*
* @param string $string Data to decode
*
* @internal
*
*/
public static function rleDecode(string $string): string
{
$new = '';
$last = '';
$null = "\0";
foreach (\str_split($string) as $cur) {
if ($last === $null) {
$new .= \str_repeat($last, \ord($cur));
$last = '';
} else {
$new .= $last;
$last = $cur;
}
}
$string = $new.$last;
return $string;
}
/**
* Null-byte RLE encode.
*
* @param string $string Data to encode
*
* @internal
*
*/
public static function rleEncode(string $string): string
{
$new = '';
$count = 0;
$null = "\0";
foreach (\str_split($string) as $cur) {
if ($cur === $null) {
++$count;
} else {
if ($count > 0) {
$new .= $null.\chr($count);
$count = 0;
}
$new .= $cur;
}
}
if ($count > 0) {
$new .= $null.\chr($count);
}
return $new;
}
/**
* Positive modulo
* Works just like the % (modulus) operator, only returns always a postive number.
*
* @param int $a A
* @param int $b B
*
* @internal
*
* @return int Modulo
*/
public static function posmod(int $a, int $b): int
{
$resto = $a % $b;
return $resto < 0 ? $resto + \abs($b) : $resto;
}
/**
* Read TL string.
*
* @param resource $stream Byte stream
*
* @internal
*
*/
public static function readTLString(mixed $stream): string
{
$l = \ord(\stream_get_contents($stream, 1));
if ($l > 254) {
throw new \InvalidArgumentException("Length too big!");
}
if ($l === 254) {
/** @var int */
$long_len = \unpack('V', \stream_get_contents($stream, 3).\chr(0))[1];
$x = \stream_get_contents($stream, $long_len);
$resto = self::posmod(-$long_len, 4);
if ($resto > 0) {
\fseek($stream, $resto, SEEK_CUR);
}
} else {
$x = $l ? \stream_get_contents($stream, $l) : '';
$resto = self::posmod(-($l + 1), 4);
if ($resto > 0) {
\fseek($stream, $resto, SEEK_CUR);
}
}
\assert($x !== false);
return $x;
}
/**
* Pack TL string.
*
* @param string $string String
*
* @internal
*/
public static function packTLString(string $string): string
{
$l = \strlen($string);
$concat = '';
if ($l <= 253) {
$concat .= \chr($l);
$concat .= $string;
$concat .= \pack('@'.self::posmod(-$l - 1, 4));
} else {
$concat .= \chr(254);
$concat .= \substr(\pack('V', $l), 0, 3);
$concat .= $string;
$concat .= \pack('@'.self::posmod(-$l, 4));
}
return $concat;
}
}

View File

@ -93,26 +93,26 @@ final class UniqueFileId
{
$fileId = \pack('V', $this->type->value);
if ($this->url !== null) {
$fileId .= packTLString($this->url);
$fileId .= Tools::packTLString($this->url);
} elseif ($this->type === UniqueFileIdType::PHOTO) {
if ($this->volumeId !== null) {
$fileId .= packLong($this->volumeId);
$fileId .= Tools::packLong($this->volumeId);
$fileId .= \pack('l', $this->localId);
} elseif ($this->stickerSetId !== null) {
\assert($this->subType !== null);
$fileId .= \chr($this->subType);
$fileId .= packLong($this->stickerSetId);
$fileId .= Tools::packLong($this->stickerSetId);
$fileId .= \pack('l', $this->stickerSetVersion);
} else {
\assert($this->subType !== null && $this->id !== null);
$fileId .= packLong($this->id);
$fileId .= Tools::packLong($this->id);
$fileId .= \chr($this->subType);
}
} elseif ($this->id !== null) {
$fileId .= packLong($this->id);
$fileId .= Tools::packLong($this->id);
}
return base64urlEncode(rleEncode($fileId));
return Tools::base64urlEncode(Tools::rleEncode($fileId));
}
/**
@ -124,7 +124,7 @@ final class UniqueFileId
public static function fromUniqueBotAPI(string $fileId): self
{
$orig = $fileId;
$fileId = rleDecode(base64urlDecode($fileId));
$fileId = Tools::rleDecode(Tools::base64urlDecode($fileId));
/** @var int */
$typeId = \unpack('V', $fileId)[1];
@ -144,26 +144,26 @@ final class UniqueFileId
\fwrite($res, $fileId);
\fseek($res, 0);
$fileId = $res;
$url = readTLString($fileId);
$url = Tools::readTLString($fileId);
$l = \fstat($fileId)['size'] - \ftell($fileId);
\trigger_error("Unique file ID $orig has $l bytes of leftover data");
} elseif (\strlen($fileId) === 12) {
// Legacy photos
$volume_id = unpackLong(\substr($fileId, 0, 8));
$local_id = unpackInt(\substr($fileId, 8));
$volume_id = Tools::unpackLong(\substr($fileId, 0, 8));
$local_id = Tools::unpackInt(\substr($fileId, 8));
} elseif (\strlen($fileId) === 9) {
// Dialog photos/thumbnails
$id = unpackLong($fileId);
$id = Tools::unpackLong($fileId);
$subType = \ord($fileId[8]);
} elseif (\strlen($fileId) === 13) {
// Stickerset ID/version
$subType = \ord($fileId[0]);
$sticker_set_id = unpackLong(\substr($fileId, 1, 8));
$sticker_set_version = unpackInt(\substr($fileId, 9));
$sticker_set_id = Tools::unpackLong(\substr($fileId, 1, 8));
$sticker_set_version = Tools::unpackInt(\substr($fileId, 9));
} elseif (\strlen($fileId) === 8) {
// Any other document
$id = unpackLong($fileId);
$id = Tools::unpackLong($fileId);
} else {
$l = \strlen($fileId);
\trigger_error("Unique file ID $orig has $l bytes of leftover data");

View File

@ -1,203 +0,0 @@
<?php declare(strict_types=1);
namespace danog\Decoder;
const WEB_LOCATION_FLAG = 1 << 24;
const FILE_REFERENCE_FLAG = 1 << 25;
$BIG_ENDIAN = \pack('L', 1) === \pack('N', 1);
/**
* Unpack long properly, returns an actual number in any case.
*
* @internal
*
* @param string $field Field to unpack
*/
function unpackLong(string $field): int
{
global $BIG_ENDIAN; // Evil
/** @psalm-suppress MixedReturnStatement */
return \unpack('q', $BIG_ENDIAN ? \strrev($field) : $field)[1];
}
/**
* Unpack integer.
* @internal
*
* @param string $field Field to unpack
*/
function unpackInt(string $field): int
{
/** @psalm-suppress MixedReturnStatement */
return \unpack('l', $field)[1];
}
/**
* Pack string long.
*
* @internal
*
*/
function packLong(int $field): string
{
global $BIG_ENDIAN; // Evil
$res = \pack('q', $field);
return $BIG_ENDIAN ? \strrev($res) : $res;
}
/**
* Base64URL decode.
*
* @param string $data Data to decode
*
* @internal
*
*/
function base64urlDecode(string $data): string
{
return \base64_decode(\str_pad(\strtr($data, '-_', '+/'), \strlen($data) % 4, '=', STR_PAD_RIGHT));
}
/**
* Base64URL encode.
*
* @param string $data Data to encode
*
* @internal
*
*/
function base64urlEncode(string $data): string
{
return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '=');
}
/**
* Null-byte RLE decode.
*
* @param string $string Data to decode
*
* @internal
*
*/
function rleDecode(string $string): string
{
$new = '';
$last = '';
$null = "\0";
foreach (\str_split($string) as $cur) {
if ($last === $null) {
$new .= \str_repeat($last, \ord($cur));
$last = '';
} else {
$new .= $last;
$last = $cur;
}
}
$string = $new.$last;
return $string;
}
/**
* Null-byte RLE encode.
*
* @param string $string Data to encode
*
* @internal
*
*/
function rleEncode(string $string): string
{
$new = '';
$count = 0;
$null = "\0";
foreach (\str_split($string) as $cur) {
if ($cur === $null) {
++$count;
} else {
if ($count > 0) {
$new .= $null.\chr($count);
$count = 0;
}
$new .= $cur;
}
}
if ($count > 0) {
$new .= $null.\chr($count);
}
return $new;
}
/**
* Positive modulo
* Works just like the % (modulus) operator, only returns always a postive number.
*
* @param int $a A
* @param int $b B
*
* @internal
*
* @return int Modulo
*/
function posmod(int $a, int $b): int
{
$resto = $a % $b;
return $resto < 0 ? $resto + \abs($b) : $resto;
}
/**
* Read TL string.
*
* @param resource $stream Byte stream
*
* @internal
*
*/
function readTLString(mixed $stream): string
{
$l = \ord(\stream_get_contents($stream, 1));
if ($l > 254) {
throw new \InvalidArgumentException("Length too big!");
}
if ($l === 254) {
/** @var int */
$long_len = \unpack('V', \stream_get_contents($stream, 3).\chr(0))[1];
$x = \stream_get_contents($stream, $long_len);
$resto = posmod(-$long_len, 4);
if ($resto > 0) {
\fseek($stream, $resto, SEEK_CUR);
}
} else {
$x = $l ? \stream_get_contents($stream, $l) : '';
$resto = posmod(-($l + 1), 4);
if ($resto > 0) {
\fseek($stream, $resto, SEEK_CUR);
}
}
\assert($x !== false);
return $x;
}
/**
* Pack TL string.
*
* @param string $string String
*
* @internal
*/
function packTLString(string $string): string
{
$l = \strlen($string);
$concat = '';
if ($l <= 253) {
$concat .= \chr($l);
$concat .= $string;
$concat .= \pack('@'.posmod(-$l - 1, 4));
} else {
$concat .= \chr(254);
$concat .= \substr(\pack('V', $l), 0, 3);
$concat .= $string;
$concat .= \pack('@'.posmod(-$l, 4));
}
return $concat;
}