From 0ece30dc7c0e34b4c5911969b8fa99c33c6d023c Mon Sep 17 00:00:00 2001 From: George Hazan Date: Wed, 30 Nov 2022 17:48:47 +0300 Subject: Telegram: update for TDLIB --- protocols/Telegram/tdlib/td/test/CMakeLists.txt | 51 +- protocols/Telegram/tdlib/td/test/TestsRunner.cpp | 63 - protocols/Telegram/tdlib/td/test/TestsRunner.h | 19 - protocols/Telegram/tdlib/td/test/country_info.cpp | 87 ++ protocols/Telegram/tdlib/td/test/crypto.cpp | 279 ++++ protocols/Telegram/tdlib/td/test/data.cpp | 442 +++++- protocols/Telegram/tdlib/td/test/data.h | 15 +- protocols/Telegram/tdlib/td/test/db.cpp | 512 ++++-- protocols/Telegram/tdlib/td/test/fuzz_url.cpp | 4 +- protocols/Telegram/tdlib/td/test/http.cpp | 358 +++-- protocols/Telegram/tdlib/td/test/link.cpp | 983 ++++++++++++ protocols/Telegram/tdlib/td/test/main.cpp | 56 +- .../Telegram/tdlib/td/test/message_entities.cpp | 1652 ++++++++++++++++++-- protocols/Telegram/tdlib/td/test/mtproto.cpp | 663 ++++++-- protocols/Telegram/tdlib/td/test/online.cpp | 632 ++++++++ protocols/Telegram/tdlib/td/test/poll.cpp | 58 + protocols/Telegram/tdlib/td/test/secret.cpp | 376 ++--- .../Telegram/tdlib/td/test/secure_storage.cpp | 70 + .../Telegram/tdlib/td/test/set_with_position.cpp | 263 ++++ .../Telegram/tdlib/td/test/string_cleaning.cpp | 103 +- protocols/Telegram/tdlib/td/test/tdclient.cpp | 1031 ++++++++---- protocols/Telegram/tdlib/td/test/tests_runner.cpp | 18 - protocols/Telegram/tdlib/td/test/tests_runner.h | 18 - protocols/Telegram/tdlib/td/test/tqueue.cpp | 250 +++ 24 files changed, 6700 insertions(+), 1303 deletions(-) delete mode 100644 protocols/Telegram/tdlib/td/test/TestsRunner.cpp delete mode 100644 protocols/Telegram/tdlib/td/test/TestsRunner.h create mode 100644 protocols/Telegram/tdlib/td/test/country_info.cpp create mode 100644 protocols/Telegram/tdlib/td/test/crypto.cpp create mode 100644 protocols/Telegram/tdlib/td/test/link.cpp create mode 100644 protocols/Telegram/tdlib/td/test/online.cpp create mode 100644 protocols/Telegram/tdlib/td/test/poll.cpp create mode 100644 protocols/Telegram/tdlib/td/test/secure_storage.cpp create mode 100644 protocols/Telegram/tdlib/td/test/set_with_position.cpp delete mode 100644 protocols/Telegram/tdlib/td/test/tests_runner.cpp delete mode 100644 protocols/Telegram/tdlib/td/test/tests_runner.h create mode 100644 protocols/Telegram/tdlib/td/test/tqueue.cpp (limited to 'protocols/Telegram/tdlib/td/test') diff --git a/protocols/Telegram/tdlib/td/test/CMakeLists.txt b/protocols/Telegram/tdlib/td/test/CMakeLists.txt index d120d8d3fb..2ac9ad32d6 100644 --- a/protocols/Telegram/tdlib/td/test/CMakeLists.txt +++ b/protocols/Telegram/tdlib/td/test/CMakeLists.txt @@ -1,18 +1,22 @@ -cmake_minimum_required(VERSION 3.0.2 FATAL_ERROR) +if ((CMAKE_MAJOR_VERSION LESS 3) OR (CMAKE_VERSION VERSION_LESS "3.0.2")) + message(FATAL_ERROR "CMake >= 3.0.2 is required") +endif() #SOURCE SETS set(TD_TEST_SOURCE + ${CMAKE_CURRENT_SOURCE_DIR}/country_info.cpp ${CMAKE_CURRENT_SOURCE_DIR}/db.cpp ${CMAKE_CURRENT_SOURCE_DIR}/http.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/mtproto.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/link.cpp ${CMAKE_CURRENT_SOURCE_DIR}/message_entities.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mtproto.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/poll.cpp ${CMAKE_CURRENT_SOURCE_DIR}/secret.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/secure_storage.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/set_with_position.cpp ${CMAKE_CURRENT_SOURCE_DIR}/string_cleaning.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/TestsRunner.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/tests_runner.cpp - - ${CMAKE_CURRENT_SOURCE_DIR}/TestsRunner.h - ${CMAKE_CURRENT_SOURCE_DIR}/tests_runner.h + ${CMAKE_CURRENT_SOURCE_DIR}/tdclient.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/tqueue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/data.cpp ${CMAKE_CURRENT_SOURCE_DIR}/data.h @@ -26,23 +30,44 @@ set(TESTS_MAIN main.cpp ) -add_library(all_tests STATIC ${TD_TEST_SOURCE}) -target_include_directories(all_tests PUBLIC $) -target_link_libraries(all_tests PRIVATE tdactor tddb tdcore tdnet tdutils) +#add_library(all_tests STATIC ${TD_TEST_SOURCE}) +#target_include_directories(all_tests PUBLIC $) +#target_link_libraries(all_tests PRIVATE tdcore tdclient) if (NOT CMAKE_CROSSCOMPILING OR EMSCRIPTEN) #Tests + if (OPENSSL_FOUND) + add_executable(test-crypto EXCLUDE_FROM_ALL crypto.cpp) + target_include_directories(test-crypto SYSTEM PRIVATE ${OPENSSL_INCLUDE_DIR}) + target_link_libraries(test-crypto PRIVATE ${OPENSSL_CRYPTO_LIBRARY} ${CMAKE_DL_LIBS} ${ZLIB_LIBRARIES} tdutils tdcore) + + if (WIN32) + if (MINGW) + target_link_libraries(test-crypto PRIVATE ws2_32 mswsock crypt32) + else() + target_link_libraries(test-crypto PRIVATE ws2_32 Mswsock Crypt32) + endif() + endif() + endif() + + add_executable(test-tdutils EXCLUDE_FROM_ALL ${TESTS_MAIN} ${TDUTILS_TEST_SOURCE}) + add_executable(test-online EXCLUDE_FROM_ALL online.cpp) add_executable(run_all_tests ${TESTS_MAIN} ${TD_TEST_SOURCE}) - if (CLANG AND NOT CYGWIN AND NOT EMSCRIPTEN) + if (CLANG AND NOT CYGWIN AND NOT EMSCRIPTEN AND NOT (CMAKE_SYSTEM_NAME MATCHES "OpenBSD") AND NOT (CMAKE_SIZEOF_VOID_P EQUAL 4)) + target_compile_options(test-tdutils PUBLIC -fsanitize=undefined -fno-sanitize=vptr) target_compile_options(run_all_tests PUBLIC -fsanitize=undefined -fno-sanitize=vptr) + target_compile_options(test-online PUBLIC -fsanitize=undefined -fno-sanitize=vptr) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fno-sanitize=vptr") endif() target_include_directories(run_all_tests PUBLIC $) - target_link_libraries(run_all_tests PRIVATE tdactor tddb tdcore tdnet tdutils) + target_include_directories(test-tdutils PUBLIC $) + target_link_libraries(test-tdutils PRIVATE tdutils) + target_link_libraries(run_all_tests PRIVATE tdcore tdclient) + target_link_libraries(test-online PRIVATE tdcore tdclient tdutils tdactor) if (CLANG) # add_executable(fuzz_url fuzz_url.cpp) -# target_link_libraries(fuzz_url PRIVATE tdclient) +# target_link_libraries(fuzz_url PRIVATE tdcore) # target_compile_options(fuzz_url PRIVATE "-fsanitize-coverage=trace-pc-guard") endif() diff --git a/protocols/Telegram/tdlib/td/test/TestsRunner.cpp b/protocols/Telegram/tdlib/td/test/TestsRunner.cpp deleted file mode 100644 index fbe155738e..0000000000 --- a/protocols/Telegram/tdlib/td/test/TestsRunner.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -#include "test/TestsRunner.h" - -#include "td/utils/common.h" -#include "td/utils/FileLog.h" -#include "td/utils/format.h" -#include "td/utils/logging.h" -#include "td/utils/port/path.h" -#include "td/utils/tests.h" - -#include - -DESC_TESTS(string_cleaning); -DESC_TESTS(message_entities); -DESC_TESTS(variant); -DESC_TESTS(secret); -DESC_TESTS(actors_main); -DESC_TESTS(actors_simple); -DESC_TESTS(actors_workers); -DESC_TESTS(db); -DESC_TESTS(json); -DESC_TESTS(http); -DESC_TESTS(heap); -DESC_TESTS(pq); -DESC_TESTS(mtproto); - -namespace td { - -void TestsRunner::run_all_tests() { - LOAD_TESTS(string_cleaning); - LOAD_TESTS(message_entities); - LOAD_TESTS(variant); - LOAD_TESTS(secret); - LOAD_TESTS(actors_main); - LOAD_TESTS(actors_simple); - LOAD_TESTS(actors_workers); - LOAD_TESTS(db); - LOAD_TESTS(json); - LOAD_TESTS(http); - LOAD_TESTS(heap); - LOAD_TESTS(pq); - LOAD_TESTS(mtproto); - Test::run_all(); -} - -static FileLog file_log; -static TsLog ts_log(&file_log); - -void TestsRunner::init(string dir) { - SET_VERBOSITY_LEVEL(VERBOSITY_NAME(WARNING)); - chdir(dir).ensure(); - LOG(WARNING) << "Redirect log into " << tag("file", dir + TD_DIR_SLASH + "log.txt"); - if (file_log.init("log.txt", std::numeric_limits::max())) { - log_interface = &ts_log; - } -} - -} // namespace td diff --git a/protocols/Telegram/tdlib/td/test/TestsRunner.h b/protocols/Telegram/tdlib/td/test/TestsRunner.h deleted file mode 100644 index a5bc66d855..0000000000 --- a/protocols/Telegram/tdlib/td/test/TestsRunner.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -#pragma once - -#include "td/utils/common.h" - -namespace td { - -class TestsRunner { - public: - static void init(string dir); - static void run_all_tests(); -}; - -} // namespace td diff --git a/protocols/Telegram/tdlib/td/test/country_info.cpp b/protocols/Telegram/tdlib/td/test/country_info.cpp new file mode 100644 index 0000000000..bf88173915 --- /dev/null +++ b/protocols/Telegram/tdlib/td/test/country_info.cpp @@ -0,0 +1,87 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#include "td/telegram/CountryInfoManager.h" + +#include "td/utils/common.h" +#include "td/utils/tests.h" + +static void check_phone_number_info(td::string phone_number_prefix, const td::string &country_code, + const td::string &calling_code, const td::string &formatted_phone_number) { + auto result = td::CountryInfoManager::get_phone_number_info_sync(td::string(), std::move(phone_number_prefix)); + CHECK(result != nullptr); + if (result->country_ == nullptr) { + CHECK(country_code.empty()); + } else { + CHECK(result->country_->country_code_ == country_code); + } + CHECK(result->country_calling_code_ == calling_code); + CHECK(result->formatted_phone_number_ == formatted_phone_number); +} + +TEST(CountryInfo, phone_number_info) { + check_phone_number_info("", "", "", ""); + check_phone_number_info("aba c aba", "", "", ""); + + td::string str; + td::string reverse_str; + for (auto i = 0; i < 256; i++) { + str += static_cast(i); + reverse_str += static_cast(255 - i); + } + check_phone_number_info(str, "", "", "0123456789"); + check_phone_number_info(reverse_str, "IR", "98", "765 432 10--"); + + check_phone_number_info("1", "US", "1", "--- --- ----"); + check_phone_number_info("12", "US", "1", "2-- --- ----"); + check_phone_number_info("126", "US", "1", "26- --- ----"); + check_phone_number_info("128", "US", "1", "28- --- ----"); + check_phone_number_info("1289", "CA", "1", "289 --- ----"); + check_phone_number_info("1289123123", "CA", "1", "289 123 123-"); + check_phone_number_info("128912312345", "CA", "1", "289 123 12345"); + check_phone_number_info("1268", "AG", "1268", "--- ----"); + check_phone_number_info("126801", "AG", "1268", "01- ----"); + check_phone_number_info("12680123", "AG", "1268", "012 3---"); + check_phone_number_info("12680123456", "AG", "1268", "012 3456"); + check_phone_number_info("1268012345678", "AG", "1268", "012 345678"); + check_phone_number_info("7", "RU", "7", "--- --- ----"); + check_phone_number_info("71234567", "RU", "7", "123 456 7---"); + check_phone_number_info("77654321", "KZ", "7", "765 432 1- --"); + check_phone_number_info("3", "", "3", ""); + check_phone_number_info("37", "", "37", ""); + check_phone_number_info("372", "EE", "372", "---- ----"); + check_phone_number_info("42", "", "42", ""); + check_phone_number_info("420", "CZ", "420", "--- --- ---"); + check_phone_number_info("421", "SK", "421", "--- --- ---"); + check_phone_number_info("422", "", "", "422"); + check_phone_number_info("423", "LI", "423", "--- ----"); + check_phone_number_info("424", "YL", "42", "4"); + check_phone_number_info("4241234567890", "YL", "42", "41234567890"); + check_phone_number_info("4", "", "4", ""); + check_phone_number_info("49", "DE", "49", "---- -------"); + check_phone_number_info("491", "DE", "49", "1--- -------"); + check_phone_number_info("492", "DE", "49", "2--- -------"); + check_phone_number_info("4915", "DE", "49", "15-- -------"); + check_phone_number_info("4916", "DE", "49", "16- -------"); + check_phone_number_info("4917", "DE", "49", "17- -------"); + check_phone_number_info("4918", "DE", "49", "18-- -------"); + check_phone_number_info("493", "DE", "49", "3--- -------"); + check_phone_number_info("4936", "DE", "49", "36-- -------"); + check_phone_number_info("49360", "DE", "49", "360- -------"); + check_phone_number_info("493601", "DE", "49", "3601 -------"); + check_phone_number_info("4936014", "DE", "49", "3601 4------"); + check_phone_number_info("4936015", "DE", "49", "3601 5------"); + check_phone_number_info("493601419", "DE", "49", "3601 419----"); + check_phone_number_info("4936014198", "DE", "49", "3601 4198--"); + check_phone_number_info("49360141980", "DE", "49", "3601 41980-"); + check_phone_number_info("841234567890", "VN", "84", "1234567890"); + check_phone_number_info("31", "NL", "31", "- -- -- -- --"); + check_phone_number_info("318", "NL", "31", "8 -- -- -- --"); + check_phone_number_info("319", "NL", "31", "9 -- -- -- --"); + check_phone_number_info("3196", "NL", "31", "9 6- -- -- --"); + check_phone_number_info("3197", "NL", "31", "97 ---- -----"); + check_phone_number_info("3198", "NL", "31", "9 8- -- -- --"); +} diff --git a/protocols/Telegram/tdlib/td/test/crypto.cpp b/protocols/Telegram/tdlib/td/test/crypto.cpp new file mode 100644 index 0000000000..1837cef631 --- /dev/null +++ b/protocols/Telegram/tdlib/td/test/crypto.cpp @@ -0,0 +1,279 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#include "td/mtproto/AuthKey.h" +#include "td/mtproto/Transport.h" + +#include "td/utils/base64.h" +#include "td/utils/common.h" +#include "td/utils/crypto.h" +#include "td/utils/logging.h" +#include "td/utils/port/detail/ThreadIdGuard.h" +#include "td/utils/ScopeGuard.h" +#include "td/utils/SharedSlice.h" +#include "td/utils/Slice.h" +#include "td/utils/Status.h" +#include "td/utils/StringBuilder.h" +#include "td/utils/UInt.h" + +#include +#include +#include + +class Handshake { + public: + struct KeyPair { + td::SecureString private_key; + td::SecureString public_key; + }; + + static td::Result generate_key_pair() { + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(NID_X25519, nullptr); + if (pctx == nullptr) { + return td::Status::Error("Can't create EXP_PKEY_CTX"); + } + SCOPE_EXIT { + EVP_PKEY_CTX_free(pctx); + }; + if (EVP_PKEY_keygen_init(pctx) <= 0) { + return td::Status::Error("Can't init keygen"); + } + + EVP_PKEY *pkey = nullptr; + if (EVP_PKEY_keygen(pctx, &pkey) <= 0) { + return td::Status::Error("Can't generate key"); + } + + TRY_RESULT(private_key, X25519_key_from_PKEY(pkey, true)); + TRY_RESULT(public_key, X25519_key_from_PKEY(pkey, false)); + + KeyPair res; + res.private_key = std::move(private_key); + res.public_key = std::move(public_key); + + return std::move(res); + } + + static td::SecureString expand_secret(td::Slice secret) { + td::SecureString res(128); + td::hmac_sha512(secret, "0", res.as_mutable_slice().substr(0, 64)); + td::hmac_sha512(secret, "1", res.as_mutable_slice().substr(64, 64)); + return res; + } + + static td::Result privateKeyToPem(td::Slice key) { + auto pkey_private = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, nullptr, key.ubegin(), 32); + CHECK(pkey_private != nullptr); + auto res = X25519_pem_from_PKEY(pkey_private, true); + EVP_PKEY_free(pkey_private); + return res; + } + + static td::Result calc_shared_secret(td::Slice private_key, td::Slice other_public_key) { + auto pkey_private = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, nullptr, private_key.ubegin(), 32); + if (pkey_private == nullptr) { + return td::Status::Error("Invalid X25520 private key"); + } + SCOPE_EXIT { + EVP_PKEY_free(pkey_private); + }; + + auto pkey_public = + EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, nullptr, other_public_key.ubegin(), other_public_key.size()); + if (pkey_public == nullptr) { + return td::Status::Error("Invalid X25519 public key"); + } + SCOPE_EXIT { + EVP_PKEY_free(pkey_public); + }; + + EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(pkey_private, nullptr); + if (ctx == nullptr) { + return td::Status::Error("Can't create EVP_PKEY_CTX"); + } + SCOPE_EXIT { + EVP_PKEY_CTX_free(ctx); + }; + + if (EVP_PKEY_derive_init(ctx) <= 0) { + return td::Status::Error("Can't init derive"); + } + if (EVP_PKEY_derive_set_peer(ctx, pkey_public) <= 0) { + return td::Status::Error("Can't init derive"); + } + + size_t result_len = 0; + if (EVP_PKEY_derive(ctx, nullptr, &result_len) <= 0) { + return td::Status::Error("Can't get result length"); + } + if (result_len != 32) { + return td::Status::Error("Unexpected result length"); + } + + td::SecureString result(result_len, '\0'); + if (EVP_PKEY_derive(ctx, result.as_mutable_slice().ubegin(), &result_len) <= 0) { + return td::Status::Error("Failed to compute shared secret"); + } + return std::move(result); + } + + private: + static td::Result X25519_key_from_PKEY(EVP_PKEY *pkey, bool is_private) { + auto func = is_private ? &EVP_PKEY_get_raw_private_key : &EVP_PKEY_get_raw_public_key; + size_t len = 0; + if (func(pkey, nullptr, &len) == 0) { + return td::Status::Error("Failed to get raw key length"); + } + CHECK(len == 32); + + td::SecureString result(len); + if (func(pkey, result.as_mutable_slice().ubegin(), &len) == 0) { + return td::Status::Error("Failed to get raw key"); + } + return std::move(result); + } + + static td::Result X25519_pem_from_PKEY(EVP_PKEY *pkey, bool is_private) { + BIO *mem_bio = BIO_new(BIO_s_mem()); + SCOPE_EXIT { + BIO_vfree(mem_bio); + }; + if (is_private) { + PEM_write_bio_PrivateKey(mem_bio, pkey, nullptr, nullptr, 0, nullptr, nullptr); + } else { + PEM_write_bio_PUBKEY(mem_bio, pkey); + } + char *data_ptr = nullptr; + auto data_size = BIO_get_mem_data(mem_bio, &data_ptr); + return td::SecureString(data_ptr, data_size); + } +}; + +struct HandshakeTest { + Handshake::KeyPair alice; + Handshake::KeyPair bob; + + td::SecureString shared_secret; + td::SecureString key; +}; + +static void KDF3(td::Slice auth_key, const td::UInt128 &msg_key, int X, td::UInt256 *aes_key, td::UInt128 *aes_iv) { + td::uint8 buf_raw[36 + 16]; + td::MutableSlice buf(buf_raw, 36 + 16); + td::Slice msg_key_slice = td::as_slice(msg_key); + + // sha256_a = SHA256 (msg_key + substr(auth_key, x, 36)); + buf.copy_from(msg_key_slice); + buf.substr(16, 36).copy_from(auth_key.substr(X, 36)); + td::uint8 sha256_a_raw[32]; + td::MutableSlice sha256_a(sha256_a_raw, 32); + sha256(buf, sha256_a); + + // sha256_b = SHA256 (substr(auth_key, 40+x, 36) + msg_key); + buf.copy_from(auth_key.substr(40 + X, 36)); + buf.substr(36).copy_from(msg_key_slice); + td::uint8 sha256_b_raw[32]; + td::MutableSlice sha256_b(sha256_b_raw, 32); + sha256(buf, sha256_b); + + // aes_key = substr(sha256_a, 0, 8) + substr(sha256_b, 8, 16) + substr(sha256_a, 24, 8); + td::MutableSlice aes_key_slice(aes_key->raw, sizeof(aes_key->raw)); + aes_key_slice.copy_from(sha256_a.substr(0, 8)); + aes_key_slice.substr(8).copy_from(sha256_b.substr(8, 16)); + aes_key_slice.substr(24).copy_from(sha256_a.substr(24, 8)); + + // aes_iv = substr(sha256_b, 0, 4) + substr(sha256_a, 8, 8) + substr(sha256_b, 24, 4); + td::MutableSlice aes_iv_slice(aes_iv->raw, sizeof(aes_iv->raw)); + aes_iv_slice.copy_from(sha256_b.substr(0, 4)); + aes_iv_slice.substr(4).copy_from(sha256_a.substr(8, 8)); + aes_iv_slice.substr(12).copy_from(sha256_b.substr(24, 4)); +} + +static td::SecureString encrypt(td::Slice key, td::Slice data, td::int32 seqno, int X) { + td::SecureString res(data.size() + 4 + 16); + res.as_mutable_slice().substr(20).copy_from(data); + + // big endian + td::uint8 *ptr = res.as_mutable_slice().substr(16).ubegin(); + ptr[0] = static_cast((seqno >> 24) & 255); + ptr[1] = static_cast((seqno >> 16) & 255); + ptr[2] = static_cast((seqno >> 8) & 255); + ptr[3] = static_cast(seqno & 255); + + td::mtproto::AuthKey auth_key(0, key.str()); + auto payload = res.as_mutable_slice().substr(16); + td::UInt128 msg_key = td::mtproto::Transport::calc_message_key2(auth_key, X, payload).second; + td::UInt256 aes_key; + td::UInt128 aes_iv; + KDF3(key, msg_key, X, &aes_key, &aes_iv); + td::AesCtrState aes; + aes.init(aes_key.as_slice(), aes_iv.as_slice()); + aes.encrypt(payload, payload); + res.as_mutable_slice().copy_from(msg_key.as_slice()); + return res; +} + +static HandshakeTest gen_test() { + HandshakeTest res; + res.alice = Handshake::generate_key_pair().move_as_ok(); + + res.bob = Handshake::generate_key_pair().move_as_ok(); + res.shared_secret = Handshake::calc_shared_secret(res.alice.private_key, res.bob.public_key).move_as_ok(); + res.key = Handshake::expand_secret(res.shared_secret); + return res; +} + +static void run_test(const HandshakeTest &test) { + auto alice_secret = Handshake::calc_shared_secret(test.alice.private_key, test.bob.public_key).move_as_ok(); + auto bob_secret = Handshake::calc_shared_secret(test.bob.private_key, test.alice.public_key).move_as_ok(); + auto key = Handshake::expand_secret(alice_secret); + CHECK(alice_secret == bob_secret); + CHECK(alice_secret == test.shared_secret); + LOG(ERROR) << "Key\n\t" << td::base64url_encode(key) << "\n"; + CHECK(key == test.key); +} + +static td::StringBuilder &operator<<(td::StringBuilder &sb, const Handshake::KeyPair &key_pair) { + sb << "\tpublic_key (base64url) = " << td::base64url_encode(key_pair.public_key) << "\n"; + sb << "\tprivate_key (base64url) = " << td::base64url_encode(key_pair.private_key) << "\n"; + sb << "\tprivate_key (pem) = \n" << Handshake::privateKeyToPem(key_pair.private_key).ok() << "\n"; + return sb; +} + +static td::StringBuilder &operator<<(td::StringBuilder &sb, const HandshakeTest &test) { + sb << "Alice\n" << test.alice; + sb << "Bob\n" << test.bob; + sb << "SharedSecret\n\t" << td::base64url_encode(test.shared_secret) << "\n"; + sb << "Key\n\t" << td::base64url_encode(test.key) << "\n"; + + std::string data = "hello world"; + sb << "encrypt(\"" << data << "\", X=0) = \n\t" << td::base64url_encode(encrypt(test.key, data, 1, 0)) << "\n"; + sb << "encrypt(\"" << data << "\", X=8) = \n\t" << td::base64url_encode(encrypt(test.key, data, 1, 8)) << "\n"; + return sb; +} + +static HandshakeTest pregenerated_test() { + HandshakeTest test; + test.alice.public_key = td::base64url_decode_secure("QlCME5fXLyyQQWeYnBiGAZbmzuD4ayOuADCFgmioOBY").move_as_ok(); + test.alice.private_key = td::base64url_decode_secure("8NZGWKfRCJfiks74RG9_xHmYydarLiRsoq8VcJGPglg").move_as_ok(); + test.bob.public_key = td::base64url_decode_secure("I1yzfmMCZzlI7xwMj1FJ3O3I3_aEUtv6CxbHiDGzr18").move_as_ok(); + test.bob.private_key = td::base64url_decode_secure("YMGoowtnZ99roUM2y5JRwiQrwGaNJ-ZRE5boy-l4aHg").move_as_ok(); + test.shared_secret = td::base64url_decode_secure("0IIvKJuXEwmAp41fYhjUnWqLTYQJ7QeKZKYuCG8mFz8").move_as_ok(); + test.key = td::base64url_decode_secure( + "JHmD-XW9j-13G6KP0tArIhQNDRVbRkKxx0MG0pa2nOgwJHNfiggM0I0RiNIr23-1wRboRtan4WvqOHsxAt_cngYX15_" + "HYe8tJdEwHcmlnXq7LtprigzExaNJS7skfOo2irClj-7EL06-jMrhfwngSJFsak8JFSw8s6R4fwCsr50") + .move_as_ok(); + + return test; +} + +int main() { + td::detail::ThreadIdGuard thread_id_guard; + auto test = gen_test(); + run_test(test); + run_test(pregenerated_test()); + LOG(ERROR) << "\n" << pregenerated_test(); +} diff --git a/protocols/Telegram/tdlib/td/test/data.cpp b/protocols/Telegram/tdlib/td/test/data.cpp index a57a9147c5..34aff9b333 100644 --- a/protocols/Telegram/tdlib/td/test/data.cpp +++ b/protocols/Telegram/tdlib/td/test/data.cpp @@ -1,11 +1,11 @@ // -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include "test/data.h" -namespace td { +#include "data.h" + static const char thumbnail_arr[] = "_9j_4AAQSkZJRgABAQEASABIAAD_2wBDAAICAgICAQICAgIDAgIDAwYEAwMDAwcFBQQGCAcJCAgHCAgJCg0LCQoMCggICw8LDA0ODg8OCQsQERAOEQ" "0ODg7_2wBDAQIDAwMDAwcEBAcOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg7_wAARCAAyADIDASIA" @@ -66,4 +66,438 @@ static const char gzip_bomb_arr[] = "TLGznukoHjpzn3qgqJTV2ogJXaK6ctqTgxKUoPyfUeqmx4szeRJuj3josnbUu0O57kcXXX-bi25IozxSONDzF5jalI4"; const char *gzip_bomb = gzip_bomb_arr; const size_t gzip_bomb_size = sizeof(gzip_bomb_arr) - 1; -} // namespace td + +static const char gzip_arr[] = + "eJztxT1LQmEAgNGXMJEIukNI0pDS0hAUSiCa4uZfELqgQwUFuQjXhkDFisrARajBocmISFA0GstBg4owopuIgdAiKE0SfkSBf6HB5TnLmYnpxMBv89" + "ExVnBN7pWvll_ePZqMpPo2Fg6dT-GQdfbA_zH_eftzVE16g8l9Ze7cP33ZTn2dlPP9XEfpXbyeqtnrm50HS7G0dbzyZpPNSkW_" + "tLmeCCT0pbrzO21otbfjqqyNuIySTjNCRERERERERERERERERERERERERERERERERERE_2k3ZA8YhRBhcb_" + "2XHN7zoR5alwbvfMtEhERERERERERERERERER0ZCqTzREVzKNyvlV8Qf1dzn-"; +const char *gzip = gzip_arr; +const size_t gzip_size = sizeof(gzip_arr) - 1; + +static const char sqlite_sample_db_v3_arr[] = + "olFZ1MdfY0Abj+LtR9ft6DTZgEHW7/" + "z7yAhC07NKr7pBAHWkbQyMPtyVSIW7PLdVaQIHYwLgd7ovQSzD7eTINxZh6Nxpwa8HTynvjhHIdQhtysRL9m3mTEj4mbjU48zq+jcFdsnzG+" + "GJAzk5MQRCi+NyDWGYnBoPhiiPc4QBoFgKoYGIhQwfVci5kf2fYIQrCM1H7QQZ8RHoqCuQ3IjMpZjt/Gzqy+P8kn2PXKWHG8/y5eDc5nQCk/" + "XhkY7A9Wl7ZUsgK7mkA1O1VHp9X0Kz/WCuWhsULuUSeshXmJ1zKciUmTMhS3T7/" + "mc2jVrR00SyT22XwpL6dRwaNtUXVJKJwtzxrTHTxWq33KdxKXFlMr5ffbWsOfpkEHlpeKybJF70bzOgKUF5pmCHOll+3gfZcJOVFI4DvHFSnt/" + "Y3XlhJDrHliXeXcfsSUNzpBsBO+/+O/MMGLi2m/kJ9GHXGpieJsHPSQG1RMNqe84QIymLEwnGptg/" + "I+gw6xqFsUqBo2Z3Hllgz0t9GqvI1ENZr16nwf9hUGMEpj2pdKGR+eSSHMac3BJaiGgoKE7BfUjaaXzI5a8FfZTyl+" + "JUMf3ou2YBuO9fiuR1AhpAyydLSAn93OdbIF+VUHea8944nyUQB2k6idA9zfOFOVPZlaWC8/KxJeLDIAYYv3s+aK3kz/" + "EyU4SSRvDLXhMdSrkNaGM4zbYc/8r4WJSAIgFq5yg3jidRq0xJ3eb+BHs6pCTz3ql50BtPd2ngzz2y51kiW2xVIP3K/" + "hI53loi9UWTIYQPxtWl8RBWD2J1rbMsG0c8CbCQAo3u0SJ2A+4gQ2q5ZRPl9rp0IEgHwhg1xtjZhz7Ss2X7HO9Bfb/" + "ioe6ZlRPPhLR5AueL6YjretqfCs0C6Y3D7Ax6ho3vae5a2HCzasqgD42OSn6YwSq4d85AurIf4GhGykmQnkHPiblLSi9LE8w59B1a9IQgrdnVHqto3" + "IYYJELdEHrEOzDOsHoXM3aiXi3kURyDw+L/ljvIwWpgrVic94CCBlwUkUCtbw+n9Gfn06GE4V1J7/" + "omTHBKJXfd1hEDBegyEf6l2mx6p6zWCRcBEIZNObMRsX0P7P9A09/ZyzQPAMJrfI25fy2PvEuHWpVmbFBm7Xsf/B/" + "pGuP3j4td+e62Fu8yhMpSNhPclCdz+MXO9yCCondvU9DUzPQxU26yQhYNasu4cLipXCYpNhc9+dpLakoqlBB7Qq52R1S/f3PgsbTYy/uJs0NwoQV7/" + "aQTgjWutM5Nb+" + "yxxebr6dfRG6HfN0AXvLq8ON2FimTK6mXa9YI5YXKROAX4VssccBw1dGM8RaaAvMKQKEjfzwcPMo0C2mCcfFB2Nkj3A2SLvWdL7APgkEnGATDjs8Kn" + "bbI5Idr9ReU38zyB2l3Ys+CVbl6dvXdCu9QMS5t9Ez6e6zu3ZJKbMDEmrD7+0QyryYwSTFMDR1LJsbHavYzfGm/" + "bAC2yFlwGuN1nqpxvHCrFJH4Gjvs/" + "IVsX1Hnr1IZFJdIaMI8DRkksKijAYxxizan62LjrGQ8Lx4SGEYqTKU182XJdG9joSEuIvh2BaLZwu6AGEO1BbtJpGeVUiupqORQMGD9+" + "jOzo3KN0u3amsHO9QASeJ2fhMM7ej7C2aBnUrsX3zayB+seS0PTpZPqhI4IImvRmuwWiWXlWqhuEMSrnfuUN+" + "oZ8LfhsUPrRriUsuM7ZQJhzeVHGLXKeO0uFhL2KW7EQs7bdnOtQw6vw2FEe6bzYUZdR7SNWvRpN1BFm48vSLnLyhjtRN0ZAI5j6FNB6sWGVXRp32uH" + "p0BH/Nuss6Q+xcn0ZhBKIUGPtVtyUZcgoy1JT23JusETl83vsEx5NkWhSUCdhThOyB/N7fl4lRgfF0VxRabLXa0RcPlF7m79R5Mn/+/mveGQtd/" + "3UurCuDd2bjJ2ZL+GNgwGpliPpjPa1LHDX7WXi/yyYHHjGq7uYuauQql0ID/" + "bZY4kx9W0YTpzeKRd3Y4FZifDtFkxbosTFsUL+svpNSmQLuGptxPrfIHadHBkwP4g5CvAswfos6FGAx+ntE9+" + "jVt9WF6n73F7awQY0RxKOPRz0ESRPI3Z3r5m4AXoaEWkzqBIcCsYLBf3gIuxQxCbo5kMxhj7+qAAbJlqUTZQZS21ZbQS/" + "CRxXEv5TIYrQv0h004kzRenULxyM2fVgnPF09eLCW1wIeLfy5q5ShfkewBJ2xoqon5Pq9MgyG+EkwZ5ZphjGTEQkRlLfVNeD6s+HBildUL1+" + "sQkICmRxIKhubnrJ0WpD4EkpvTOEZkFalGjkp3t1L7KeBseX/qFfCBlcNThLbBVZQMRLGeCYZ/OZ1Z1qkN/KB6ltAjI+ZALJ73G/" + "PzPF19S0vUmka0Aeq+Q13X1fD27HGTIW2zVhgQvgIk+shlQWLCDn9qDwNxyGWNplR8dIsnl1lAIVXokjA+qIz3RNAkwDkk60x+" + "rZKJSjLiy3RtbJ6ZTsDl4U6NIzln1zbPkbS6ttbtoI86aBtSQribN7zoG3zG9pkdr+" + "hGso6jP7QhhEVskGfdbHTtOkTOhGqcrg3jALtv5J4fTNK4kzyjJRYQ0HVuWtPMvP022bI4uA8SbjnWkH7Z2Kt8LujV9ji9RDs4+" + "Y3bQn2TlPiShyPeOJ0f2Tlaeo2liySol7PZT1rbDjr8Zp/" + "UIbkyeEl0BIg8O6BAJ3h8JMXRQTklu23Fkl6PMHsGHcz0GE58+WJMgM6RyCgc2ka7L4YeGu7mPjJRSo3umdhi2GqOfSgELUzWr/" + "gVmFljsiHJkg5xMrksDmGoG+kXv57iyNi8Dphr6SsT3lFdSG4Tx9WeMSSNOngSyK8yzJStqUmq5wiTNeBeIW2KgSQMzGETMpMVZqoAX/" + "fHdgAjYxwooM15G2R98Vs1xlX5fjEHz1DNJQ39pSPBJInvdZAQqDFVzD9A4IktiIthEIWj0RemW+HRQKQ/" + "rODTVeZB2bY+IAx+qr7JyUPkYziNWQKnXhRUm1t4ZJarr1Ud6/" + "G6RxdNjt3pX1HBfn0t4+6nOuWA7MAsdA+EvQjXd8XPLPEFReAHnIXqE0DAAQPmRdcnfDCfXtPQv+" + "hD82s4sQCsaqA1dwV3CsRzhfWiZlQ5iMPJtjU0A3UgGPg+" + "7MAXNfPUV8eiRyCwC4fEnSkj2LLMf1de1wlyjosXcM6GTMroBo8wFsUWxdTSGA1xsPWDAL3DWi6szxVLp7EGNssLZ8OWKHALYy9P6bN5qUS23FahYg" + "Tu6jVCGVAuCGwnaPLGi4yUdQIOTvTEgJiPNf8jZ2wKiOZZ4belGxh6H7Y54kfVCZwxGdhcyktUV+H3TgBZmR70x/" + "SF6ct1GeZd1W9IxEJTTcRc179RNcRk4pd5KjwyIO6/WFOtSRKOmPs+cb9bArVV/" + "HjeIDM0rvahdWvqjMo8q9BSyzjvrvLrSxkaLskfhu3KHnfftGZdiYypGsngBFN9RDmSHknNttmal5tnWZP/" + "5RhfDnfnDwCq9ARP3USRkaUpKl87l0qzZkHnSoKBUpcQ70qrhrMGyQyn7dobM7mHw8UZ4fOuCxY/vEQ3dql4RsczsaqHsl3z/" + "AgXKhs2+MI32L9lC5RSz/" + "ix2uzbExljLdYgUe6aNsUqKbYkZfugFEbTtwHXPsrSioXuusKGX00muMADmvxpqrVt63omwKlK4IJ9la10DCsJiv5NOFsIZhPTIb2fzJUR6eSRyNFL" + "Wt3IKpVTwsLXqI/" + "biB6vInyvJvqNWgc8wSIQWvngtSyJugYmFG3k9GuuJQ6VdbafPAR5BCeuJy8JvX0dANbaRvsZPsLWa7nNCM2iwmtkNMMLdQe6+" + "DdCHz5XGwW91spC1pORsSTM/" + "pegYHDptkICvczJSSTwDs1aATBZGMfVtKzed4Vxx9nxN23uqwcVCwDljkRB7AXbFaCbRLi0glGmK8CaeGq+KAuLj7eAm8w6ihS9IWGAD8oGhVyJR+" + "D4U4ah5YX/GAysJA8aK3g0xKmUfveHTUQKDs0dzdTOrTr18++t7IqK1x2jDCmU9/" + "C6saN75gcZr4Q9QyULNHFgtXCpydThPIMBJ47jZFuvFSmoBbqD6m8WUjZjDJ4+odZISIGBzUoCKut9jjok8o2RzK6oY265stKOE0Ud7EI+" + "7rAtQY71LhsUSC6Sb4QbIsJlG7vb/" + "l0fF6Kh5IRGrzNVBJFsd+0ws6JQxDEVFSuO5UZVviYt0GHS+Qtt4EnoWbARCPCQfq4EiZJSbUhIPGWLoh6ozv/" + "U+5LArEPD7JcAXvpe+ZwYyrS5c8HWtMng7B7eiNdfjv9HtpfTVidxawNwhgC4igZ18yigsRJvyGuqpCipA/" + "p5sfXFb4hibSZsif7HAPja0G4CFxGL96Vxx1eKiSCNyLYL42m2slvgI6rvYKFTVvdQtBFhiav7vg+" + "Oz2wwzYRWRlHsIdOfcNws8XAgTL384gT8km9UtO4WtUyJZL5xDb2iWGsSoqRCAFiNYsxpwcCfZa4p5YP8E7PX4tzdUJ+" + "m2XNxuM7esUyjDouRnVu49PP78Q5mFAsKUZ/4mwPGlzInGVr8g7piw4FueB5qFfCHibo8gsC355UnT6WB9Gdu2h4+dsI0wh51a5UW7/" + "GY8zQ7VDmorGfSiELm3SpS3xvRhBOHGBLgn68hyFNMpzg/" + "XfJkEUUlVvayVpmJsKqeQEVwPzLW02XkvsFmjY98JIIDg4PqRw+2vWTQu4yHDx5AwtAYZaCDUYS10OjYZgzVp0NqlA2Lmc4oRYRumyr8w+" + "gNFxX16vxrG6DMAJeMtYyACbiuAp3RsW8G3rAWifNmTx8mEkTzZHW6Xqt9nD1qPeVxCZMq+wq7LH333FAVFW/" + "V6qccLaqy2uquIW41fWrKW3MkrD3fKqjdGSz0rj278X4kLakpOVvr9ww6FNk8wRL0TcfRxurhhLtRLQp6zgyu5BFA44xFIbq+" + "0Yvmics8iY9He45gODgaWRB9vfM8JhpTpI6c6ovoxL8fOuWEPzadjg388Lhaqv/" + "CJa8IVbKO9BBMrrHQVbF1iVKDO5UrGR+4LY7NSe7Jt5vrwzhFPaOhDw6VoL3p5jUq3D7ABG3tKbcdrrL+" + "lCJ44J0T0fYMVlzFlRjc75oNqPiKDyKOI1e9Fvw9ybZw5b0iU0kyBf1hW+JTtgr/qsszaWe/mHDdGVNXk6OBBCzECTrUj4WdHGi/" + "VorxztlwJ5GXoTTdFMhckg//ExJweev/" + "s2aYgKZRSgZVkpKHVqyNTVuEaB8UQuPhGGwEerawjiK0hRJaF4Bg9UtJMPuIu8V1bis9oCjWKVcqgiyam4t6N06O5tM38wfPTWtyD4b8ZUDl0koAfa" + "n9+uE1mJQw1DSMixM4QDnGXFA9bxgt/" + "LvKWdz0MwN9wgI5yhYtmHau0l5lO2WRsRhZvJJof1z3LvZCEqQB2F2z7GHuzmmNnyvZ8QqrnV9EBScfY25vjjBisylp2dorLh7oI2W4z8RsvegvXn4" + "nsh8bsEj1DJtXNRIHuhQGY9ewKOYFTc3zUK1NJaNpzBZmW8ccWYJYi6Mk7ZLfpL1A6V/C0AyePBAysg4r/" + "VoaLnZxgvhCZvjTH2uqfX7iQuS4F0t+qAijrgPwmm5Vy9JJrgWPKujTZkCg6Px0VVtFC+EfJSx9QyvPY6w4St/" + "0D4EjTxRXuEoDGkr1oYBQ8kVBj5KHyIkHCEEBAmrwA0pwtdOyzpRMj79IahmbNNuWftCHx3LklCpaD3Yo0pjTfML4ToJl5S2sQ8wvHsd960fr5fdXZ" + "EORmJtkLLgu5Ca6eiKf5GIw/yKSLUwY0F7rMvySfHB3PzBjJjgnt04MzGFg9vSEt6+OsRMKhV1eRGgPpLai6WzxcGY5vuiqw/" + "14TsoLKeHbkRO6qJgiJzo2HlDc2+MCrYNp31aeyro7WWQr/" + "sPybVj7BVFLuplVprsOG5TWwpzP84I3euP12UlSUma6TgVgyQmoms9rutumle3d26+b7fmnS/aQ3ps9kqYwN0+oDXzkQaP7apliI1Ks/" + "3z3ZsGaiLlyVSkDxgLmov1p23Qy7yI1TxxsGt4hVyD5kSJIxzoFnqT09wKhHHa1/" + "smUNIqeDLoo5L5e+TiBEGsiHWPcG4bsUWBF9NkbjFxp34tnO9RlE5cFALntzx2418tlQ+" + "4CnAljWUAiKEFJrHGH5IBIOU4jMiHmA6w0WYa9bzTnvhaRXVtxWKK9VuNbkciK8+PZOVEz7F/foJ7ERZ2P+9w8htm9J/" + "rxKW17GAJN5VVEotpmOPao4PJ/bi3x7Tt/ckZr8444ax/w2BZw0aLt/DgA6Kd04+MOr+pEpi9J3WHKvNnYGFWgYvSg0alOsp/" + "Nnwjo6OK6fvTOokwi78PxOy7+yQ2uWZOTzDkFnZ1Ri7X11X1MlAXqGehz+QfjtF2FcPVDLVFsAiLlt/" + "Di7LUUhzf3xitZHFkphtSE2ilrIgQAF7cPeRJBFM2MIXNhhlMfAgychBtUcPgzSzkdUbRpZ2pKNtpNZr9Xq5GQLIcKJ4MkBOBUeoduEBjWPxiGcYWo" + "tFatmZRoxiYAHxNTcN2FrQ0I9E12UC3NcaFTkqvCaBonx2CvBayKeXuewhxbLB503TQi8E9FSrsi9efYPhGDqqX2EsJD/" + "3DHOg28NZvOpZGLs9AEWpgrl+x/" + "Wk6mXCxM++OZxA8K3MGlQuG7Gmodcz6FHh9mqoIZZh6OrObpBUrJfdoZeWXR+" + "GVt8zi3m0oPlAhNUyi3a6zeZcvqfwI3M7zoXxGU2q0ETZgfCE26H9E+PNxes7mw4SwEl78lclmnNhUlZ5C4Y8v2YJnmFn8+a6WdrgjTU2awQ/" + "osSJFtKuNgOw9n72uyhPOkEB4qcVZ1A="; + +const char *sqlite_sample_db_v3 = sqlite_sample_db_v3_arr; +const size_t sqlite_sample_db_v3_size = sizeof(sqlite_sample_db_v3_arr) - 1; + +static const char sqlite_sample_db_v4_arr[] = + "6YZiAqwf2RbWHjDRoAmm2jMPm70HtfDOgIbyJR9Btp1RWtJiNmwmqsZCfh872BvqFVfYdhtfUJwVIpOe3se/" + "9d9v+" + "eMJFKyUxyxkeWjwLJiAEa1fQtF9C8RVtmiTUweBQFmuAKlIVjTHfM0C2IspViXoBYMElb6CpuwKIN0XptduYh3GPC2IBjBET0eGWycgrOPCeTSe8PC" + "SfBtaX7dlCOE3zTeRqR+iP3G5Tb67xB2e8CDp4EbPJiXbNwKMyAek73D8dVqeFcjtuUfRkah/Iu6hIkBLKiS02ut/" + "jD0b1Sy5RyR7NDiCOKVmqnwwKDrcDbofPYmll8ilK4/qzcmzRBJKhxJeaRRy1X9UvzSbYaj22wCKrQLMqHeZ7EVOsPXJEhLVsSWnym/" + "vkl9mLxhiDUhvP4pNK3KjmoK1bTEIpqABLhQELQjl+EzBDt+" + "tYjeyZ0vCXL2JUYScZcWHn0kD1jwIYGm7r4QIfEMOwwPkkIcsIUOQjW2ICqCVfffoP5JQO9e5352tpAYcUYyiagGKDYtt1yDmFqqvL8Jn+" + "r19aJHMsQgUgE7dEusHXiefgeaVRmmI8gW88xvYqyPcM5dz9t8eU7jl7ipq4EPYPOl6K4p0OU/" + "Ro2CJTekwHkaouF2T+" + "L9WOuRT5WgGsUpcvmY7xDtmS85Iy3uQHZuNeL6FWSCJZ8qjbFbx3zKrlSoUwef86H8OGNA4rYmpKaEpQAsiI1IUi21q26HlgjPE9+" + "m2M3HZH72fnJ6lZw6v+vy7Jz/M80nIv0A5A67DE/" + "Gu7FPHx4NmxLPbU3pWMIg8fUl3uB5Pmk6hL+Gxi2+W+jvpLk31Qk7HPDX2H+bdTEgetpKzgjcoh/" + "XbvrBS82jWgHpWzw9vlbd2FUKbT20ULRE9zSHFYbdWmqchJc4Ma6iNY8rDgH4slTCt3koEaIXTwCsBguVS9a/X1QLUcWlBzt7ot5hovXQUuKd/" + "XHDRvusNMvcyxSOwPpF3tbMO9or+BKUq4nmwnzFUzzijl/v0KYUaTmYXQ2HgLW71oUl0cqdJ9cYk0s4MSppzo1wYF3BdQ/" + "348785aLf4O3HwZSXfdu2juvZTMkksahRiRCQgUDaB9izTwe/PyfRYv/" + "fm5VY6xzoQc3RiCa4zAQBDj3JffYw6cAYk9RHh290Gcvf8QgfBthp7dwlS3XzTNsubyd4l4FRQkvpWInHiFEUPd7wwHQNCnxE20DK/" + "S8Jw86Q7VpGnmPlVGQn/vdW2RrJ/" + "ATsQnBQPUb2+" + "vIoVqFRhHWk81Qxp3LL0Jy1xWe8B7y7L47R8lzI98vOwze7KBr8IH3Sw0snD86vp0kqTqnmJVYrWIROMYm9be7dvc9GyqFPF3lAhnApCetJ4+" + "PSkh0hnW6a1rwhb/r1QFBrhChatbFGh2fJ0nyGV5i9+Y5EzB3hdgyupU5m+rulS2DngmhEl/PQ7L7ALIZksbYwi/" + "1O5qjRKGf8pp93LhMysP21f2GU/Yl6JX2QZT9139ag1M/Kh/" + "pqYTuqrLsE38Lc878Q4kcBX45tklajnY6wGCzqSCGnhDu79SvmgE8Ymw1klxen17FDtgjqJ9ZnkJsd4RoES9kGWyI1zEKFFVH7yq0K2Psl+" + "ft8MQjHXNn80H1plnpQ7SRINJZBZFq8PPhRVuiJdO1+byGy/" + "J4TLN4jrc0CVIufAa60SOqU2S236nXsF0fW0b1V8zrgEK4qULe7EA+ubk4RxHZIotRffpHmZS5enm/r+ENZL8lECFnnUPbOQ5VhL/" + "J6ICCsXykofIKiYGcegY+" + "GQD0QvqEXoFDQQeABbIzeGB42BqQRJzMFnBCzHoSaCDOrrmJpvn1aPZoJrdejZ6DRkDLauuNretHKXyqQNvoj6jNZzrhKWLSIoqy4Sk6bec2KGjX2j" + "d2h9U1RahlGIR8JkTP1LHmLnq2ZqHNyDgVfDHnHEeimbxrcv8vEtxxR0rJ2t0vL5dAsC4cyZTtBfX3kpOMjVZYH4etX1Ob4mpsLbGX+" + "4xhqP8Tmed3gfurVkremgANwmJO0cn3/CHI/5tzsJiN5/MN2zlADJGMUYASvOPNUvxtS7rfFu8dpRpLOQt5nmlx/Trtdomixj/DlHCu0hyK9j409V/" + "SpChv3oHn6x5h52IMjULbSyUDYbJigvTJVWJd1CZ9Ui34rKqNp/" + "R+q7TtBCioHuTDT0J8m94LO08nQyAGP8ZiS8HRlcfIRorLy4OXrQ7TVaYBC7eXDW8VxkxTJIlBMFQzRykAC4/" + "AXBPCIADMlnRr7ClxFxJeu8M6QA4J73WyaWIuRMN3xzcZ9xzAY1xQUMx8gF6MRtB7CdKB/sRSDy0+L0XN7dXhKDtlUykputDeNj4/" + "ff0h8YLW3p+aozOh42a44OhPj6IwkGee9R09pnRGPooY7ytNMpoXOMnbZ4UJAwz1yhDKtHV5DJZD+ubjN92SDrAjpDjwjEVmde9F4kbPPUlJ+" + "jDIeeVrAOUtmlL0owMk4yhRDYZzLkstHqsLUDKc/" + "YWjhPBWwzMcwdDvDJe6ygkj6FSv6OgKCuHVcZqx6McuuYK4dJXpneTnxRyDvzlLdFvnB4ZczLxgYkdQfHjzVfgKILYLWxuDsgHuLJME+" + "00HZf3ueP8OH/qF3uhfMKOVzNPBBHlwU4Tb1s48XogYWqBoaR4iTuzVYne6CjbbgAMGi19X8J3C6BImIlu7eOs6O1wtXeYCeR/" + "HVljBjQKTcfgT743P9z5ZswpDVb7z2altSnx5opnRPhQnw/" + "FEGy1CasmBRUtPBxR0ZJpD7w0AqDrd6PPEsqWJIUqs3IBzXNVN97nV5amhJG3ZXmDwC5/" + "myWCszP0pXKxK5j5+LQRuEoZ5SaVWNAP7WIp5zb3m2qNbnEOUlsOCXqDMHakgQWyQt98zLCvB/ekO9/NESy53FxGWlst7/" + "P3BYzYalrBwpeWiEtNHXhqWiRhjWyKYghMU2ydsYGuS5huQPUEYxNBjyIuuG3N4UF/" + "PJcPfZG7cJCy5HVY6SHA874Cd93biGb6l8GMDoYtxfTT6YTKwWxplNOwI8fppQvc9yuFexiLJFrXWFWSFMxTE+/" + "l9nEdCYT5PvG7EayejuXvEXkK2fUWlhm3BXCIcQI4M18kN95NM+fTFTpC9espq12gyh6vIJv4e++" + "93SvbldyE262VJzWFu3AXnmCLevgBwJr9Bu5QpQ5GBsDc0sniAxEZIyGrSl0kOvozlZ0D3nqVex8JV2/" + "+ms8IocZlfUbDPO8W0t9M3cwdGfXT5Vjf4wVhyX+mRih860iz8Gesub26IUt9xeqk+pAdh5gV+v3rBoYzWrm3/" + "fPr5E6LvOqziK+9L2m3nFCEPX02oM79QfX/9plwSH5aMx1iY+LUtWELbOq8p1cLc8tGBWP839Z3RN58D0HRwVTonEbxP6vYFte5Fjhr/" + "ljlhI0JpRcqLgJEoaYoWOLgNcrTiCUSVMtHEdY1OfwJlHWOXfQkXGBLjNnseVGEHUe2m2xSG2EyRaC+Jph+jfLlfbu3WPEh1ZgS2sT6R+" + "9cn60m2YoyADp1M8ZaJ/huIwbcpiE3fPtVlPgpoiuXUpFpFAPRCdTQ87vK9dQbWwwfgttgbNQW9LwxpbTxtmUdGFzokw9tV/" + "joebqeinBw0R2iAtESjbWyQKbhbRO9mJXVzb5IDkVRH6mkVLdkpShRAGd3EnBQH+et75wPZ5HOuSDWFVDg1GuJs3j/" + "8vCZI334ABSiPKuvyNGnkWAxxsCAiU+ekeFJ8GTK8qXxRLGhQRiDCM1ly2cB9TXqV3dT5+" + "mgXrxbdXUPV8oqFwJhBk4WGMbzghfEMPbrn60wL12Rou2w3SnmHbPOcHhu91ENmCRTN0gaXYzqE77fQ4OUfTwQR5xTtyH2Z5VSZGl6Z5tR3KunrOg7" + "27g8BeolNCgMLOgonGQMsuf+Uur/M4vxBMTheHNaKc4roUfB3x0ruRv6LGRygPRigtLOLEwehjYOTK/" + "ZpZ4w6Tu9277NAVQlNiD+yOx9lq94jXcmfTw7c0WtAPb3wjVUuaIpODD4MjYTmcuQgd8iRHm1htgXoAJW4dUQ/" + "z4benIDjcikAGgxEaWQr1G3pXHlFFuz1uOCv98d+x3wnTVaSerAaj6lxf33IG3LnwCalCL4DkKyLY+" + "MeiNtcDxPKvVSooXxB7p8r2t8Ljjawa0SUy9ob1mYPLkuOMyigr3ozWKsBH4RhuCF7hYiGgwMXwSH0HUSkoo+" + "NTAEndQNU1ayjzJgnfN6QlXWmspwu5hIbYtSmMmf+YjKHkiDeFcSMaZ742rR/LS9O36UA44vpjX5gtuwRp5Cx3W5ginYwTm5F9/" + "xVpYzsfabq+vpIoEKWpSyt75C3Fm0CSxNV8zEvcMEiZL+" + "kYXcsIGEWAD5bs66HDjwQ4nylBPXZn28v6E8PU1SKTSTPPK8U2fpbM2HVnnhUJzhAMc6ooMjOB1stiTPbNK+" + "t5G6GbguKbW0OH9COEALEpZ9WCAisRJVUVi3umqdzIUuOU0TR1LSPkdw58wUVwNko5Y/mTqET2Ew/8sbb9Lo/" + "LwABF09LWdz4rnhjMviM1ScKkv4n7Q2EeI/" + "VWskgjS+34Ue0xDDp0+JZfkS3B2xfs4eh9XHkpig95sedAqfDOyHXI4lX10FushcbP5fni6CpFpKxoQheFv8Ek8AHgeRlXSFn8MzE7Gl+" + "5bC7iJ2r1ZXh0lUeVDUwXgM7EHQMqTMwB0MxjJLsIWtGpE329hbAJvSvJWI24InXoa4M0jGd+x3ULJ3EUITYiqf1k7c+" + "nKYd9jlaWYkZKZb7xHlNcOrVWLYl9HqiiFICQpkcWfbbqElaUUmWWRrC2/RhQZQ/" + "1QUXa8KO61bZdH3oK8VgIbo2MGNveROktCFiyrHhGYZ49WCWznhRd7w7CThjXb89BdMyXzA+7kiBkr894Y1sx1gx+tzksmsKvY/" + "a4fOkDoClnVV8T0EF8AwvpkAJyBjF+Cgc72iXNxPAPNnqOxvdC/" + "ySZmWOPN459ctwNPhhbUpM+nOsGD0VaC1TlAn9Balw66P4ETQH7D1OjaNf7zQ3lc5yhz5Sw3a7hZLDu5QwA+NK/" + "7SuE1+xntGd846b1ozytfPvEW7w3paf35d8NpIUFs0fCzF7eG76vNp4hYNy88xi/zGQ080HfGk/" + "lojlEpRqjqbLAatkWBUuuaKKdKIVf3ykKHix3+eW7zyY2G4nCaM3fk+eOCp4+v85l37n7/" + "pLWhn3X1HA1OvLenhe8Cmub0AT1IDDG4NlstG9VRDoqtXOjizahX8BQXXziAqhdOUMObMrXZlAD/" + "ZhuOBxIDlg8EJCUhUzqTGWnX2TOA4RaceCQhIZHzoYTCHvpUtIAOTqdiFOjRPDMuUETNNtAFk8N5vaHqlWlZIltl7wzEd0mJc7DVL5ht/" + "nhcvLWEhfOZAZWVMIErnjyy9oJuOVU9GYsiRDkkvRmIBAk7ziRwg1562jDtYskgrPeVm56O9nODyQaEFbDpH+K5KmoWlsAyA3btgkZ/" + "Vp0HSePyXS3xRw5Aqa766McOpfDXMQe/" + "QEc6+" + "gTpjjWPa9zMgmMTOUEYxutV0iMFC86LWjsWuDnj3Uo4V1tNosEPebxYKkePTRxArsVV8AbDlKNkj9Njh5EvrxJv3Ukg5eLvXDZKvcsdOsaYvkRtkC1" + "8+mW6hF6TVJ2Aj/NJ4+JmdM6gdVp0QHBaEAgh5CGQ/" + "piPFthNmvCRpeDroD8tQD24C6ssTDl6pysRNorhzO261iGCEe+" + "uCdhnoBjsjZzfcPYsKEQN3uMR6s17FGESaCbv5Dhm0RzInu8YtUFevKxvAsGoZqiOXxhvA6Amvo2PEWyQXD2lS8a9YB1MvUTkY4aAXQZxynRb6STnu" + "bmzl0HewbhXSYQ+qejqdBRxCbFhu/uMNnxZbzGjBVyYNPPMgPtH5glPfLj89arPr12nxVIV4/9CfGiu0STliVhZLTvh4xS/" + "7lgXC9NqwD5VfQNcFDn5ybgVbWeF/KF4k7eHA4C3pPTpgEK4UQKqZeUXuZap6xUgmPkHAD2tY5S3KhaeiI1GURDk1LVEuYMqQYg0SszamHEf/xJHM/" + "CzmFENCzfFvW6iJw1f/lrMHDdRJmjPRB26/7JPitYpRYWASG1FRb8IzaxqXJ3vFXBn3RMpY5iTt6nk8mWgwLCjqLiTUZPpttGLEgrpeb5e/dD1P5k/" + "pIiqfQobv6DfN78w4tcQhpOI0Y0SwlyzKjLjohUmKEeD70ZRZB5CC/bT/Uk4lry+bGdwfdZg8KvCgT0P+/" + "ciaPyARWKJxkjyUsB7meweTZncXsOy87kJHhRtK+YeK0Ee0qW3q15L1CcbSWvAG+GdNjzKN9qpFTmGZaOrKUyk+QzuQp/" + "jghdOOe464NgK+V4LL1Z5kJFpsBhslpJY24E6i+laEIPi03wFcztYG45zi9H9IwxTEUifoljWEbcnFFYfUh/" + "hsAiUnNDdMlsqPwyA+KMnD9kYyTM5EXZ+RhkVebpNeBHjtUSwoTO4Hyeve/DrKs4BMnokPvbCybs0okso7JFA0o9YLq5/" + "VMh5Iihxyq3hxfTykJfbGID0TMTbzGQ1zJW8wh322l14VmnNa/" + "rSSWit5LsCCqgrlruqIEjD8Myx4nB6uu9Z4goffZKF5vtheukwq2fDEF9IsgoVQu4byzsIccL3OQ/JcXvXwjrrkDa5gqZKn/" + "c2pyVwQF0vLf5O+xIiskza/vu2KdAfXdjY2GA+s3GZeBauwyM4PKkgNwb4mdRSIxjPBd+lcu2CfT4YjeGz7Ckn9f1jaNHUEcPqGYtk4x/" + "AEwereiXAi+" + "CWxhChrjULbs43IDlcwHUyTXiltyCJTyP5KakHGUgQX80mcdUYRw4DZggwoBUgAH3VX9gow3GJgMoRfegqh2fF6ExNxcBDdL5ybHirQN3Dg2RKh1WE" + "G2KLKS922uj3qSZ9VwbAp8T5Y11Z0Tp2QataJuSlt1Yvv22SnXBHYTVvg29xdT88ZhC5ICiFqXWLc1x5WgL413ul7oUcQD+" + "VZeGFBB68835DbTDoghbmaklEVuS8Yn1bwP+tAiIK3QOFpJ4+" + "JhCRqPf2grJEarkcxuKvc3T6EoaGQnSKoOUwj48AozJAH1QGamhV3C1akpqv6f9rtidXuVwx3BAbYAIe7HRNZ+wA0klnmHjkLcsIIOZ6lGbN/" + "Iy8luQlV0KS4a1DN7NrognIZ8d5zYZc+vS2qe7V7dxOp5mrKrnUDA+lZYZGVt2IoACYVfmNVkQULqs6+" + "FpfLMBWp4Yr55gt1riZCxXJ9bBgMCBzt1PTUVsHzZW+q22S05MMgBKaO89yTEU++46jws+W+7QZxi7BwmEPrpj+" + "GuuWxtnBjdqqSsOwoxVendEeLbhOm+cXSDSnXuNQE4STC/mF3DyGrHHpITdw2yLpltGwNnICiTp4slwg0Htsgwd72Ciym9Qvv+rzIwN5WR0PT9QB/" + "K5q4Wu2u3yxzXHPF0qwhJjVPec09/" + "QeBFkVo8eR1mXG9jZhNcxPPYP10wqAMVtgL65nhzzEDyS9cRoovNExH6LzPz8uMUGhZBuiN1crX5EqkVeJ0WVQbj/" + "ixqM9vpxKRG2WNX5A9jfTjgqx/" + "LqAo2i9Hf5YPMuJdGH2Sa8cwUMxlNNsjdanERNuhDF7TFeFM97hXGhVRGn4nasYEQmxQYepmf3ZhDOPk5R99gJZPPDBYSB+" + "UfGmqkBD1imYvrviGj0KATGpFveUmwa2BHv22gMRdidv75fEbVfpbhvHU+Vj69rWBtrTSAodscfXvmwyOPJOK97Z4hEUA2T+n/" + "8x2P8NYjayHJv7HrYID130WV3bOJWgAUGz++314+W7EN/" + "agjWDVH78SbiaquPkdVTa4GmBu4hKDNxsZYm7p6ridKNQ6dqKWUR4lAhFyUug0ohYL7DytzCW+36ICW3AsZmoSybyvI5czXNTx0+z+" + "zo60gODtb7LbqNb1Ff2ixlc8okhgOomHg7mvAH23t72hgq7Xl9TFk06pKshGpljpm38G62rihKxgidJ1/OaGsrobnwRmEbF6PYDqjp1NAORsG4u//" + "c8wKAQlA51saIpVkHDfQemKwGuLsg4t4zXkCpQd2mW1QP2ZzOOS60qG/r/V0DVVFELJzNkOz8B3feH6CjDVgACgIeAlAKYfTS8JizfcCbVCFpv/" + "kaJ2sTle34TxH3oMKfJYFJ8kzXqni4UAyF0jAVJOblTbuySNKKkODQJjOa+rmivHGMtMS1o51oKU+vzXGavTm0Op+" + "1wTsOrlDbdv1ItenEycdXQnLci6xHT98AgjVmjBAqP5RKZXEEAtokNfLkq9+" + "GekgmiOPTtSbazIsuBFM7ZbqrhuNY3ielOPL0hO6PGxgHdBA79KfhDVswMtsPT68L/I+RmaEA571TgzUWRwMC4QlaOoPxJULJxiJ187KeXV/GS/" + "e7JpGQo7pAJBZfamo7+OLKeA+QoW3vIWWnKabByiKGgp6rpdUxPGUSn+/ny4UAV1zsNB2jMaE/" + "1OVxznFDpgsQ2TiYc3Bi6dzrD93xWn3BVgobD1oFdXynBWLdFn+" + "Y0IoN5GLCSElZ4h5IVT9UB54wZ05hC3zY9qOi7VWDSAQ2AeegtNxvYg3i2D4bLt3mEd0+" + "3PbhV1DB1XpR3Gdoxr2XYifvicmuxNUCcRcjAvTO82IENMWqd7+/kJsIW8ZJWUzP6BrsRaM/" + "HboweCRehOWOEftlwNRYrq00h6gkNqGUNtKl2CDoVqr28Anbn+5uKmKwLgf3Lq3Q3bdD1zadOc5fI7NELLLGZ70WOx+" + "EORCVRFDSDoDKQwdixEEJiJZcEhG/" + "Sai+" + "4iYVrRQ5hwyWKHEVrGcE0YPsT8PN08RDb3HR6L7mPNN5KIaKultAxESCVQMbTYeLKpuoCesrbWn7xtejikKGeMEyLI7bF60PZ3DBNzYF3jnEEcwj5P" + "d1sTubA6r+D7tpzaZwSg+5VRD4PLySG7hmT4BPP3QEJvtxfNFTbdW0Yp76tSZJi82zTVY9EgpGZ5t68ZhQtzmkTUmtAQfAuNEEZ+" + "UE8Uuq03bEkSlCjlyUVWedQT1Qs3+oT7DwOU8ystBo6HiwEji3c5sfopnwQ8myk3/ADrVWcuCPtIB0E5OtoJ6IpwIW7enMk41vd/" + "wdAKi8zFrjslkjZ4q1BKPcw4TR/" + "UT6P0WLQhfy1qV2qBiZA1Xjzb0to398aKws8WY0fAtR8OTJTmeyHbo0QlzhkDO5Ui+6YO41lYWC3DLzEdOQYKHHiYNJac08ai9PW5/" + "lgZwfa75WrXnrD9C5gBtd6h8S6Mctq/qLhXpw+3B4+4XerXf7P1UXQqsKioSXR82tvF84BmCn5XpeDjXm4drhq/vtgqI7780RqYz8OutXN/" + "3WnLx8ru+YpT8/YZqx4B+LzKO1kGxIxP5662vIVA8arp00OhEe+e+Ss5PQTTvStEybK/w9bWhGgZHgQEu365q7TeGXO97XTibUTU/" + "hhf899plPuWSf4jJZFbRdTq4OMDsbAi0hDnsdhb8KYjgRE5dnOFqZr3T74Z18OB2D5INP6qBJN57su7mYlLxmsEJlMMuub3KwNUHEQdwk+" + "7FACRTTIxi0rTGw5olnZKXOTiGVfI0OSzjSeSCxEruR7IUdNcWVJKc6CsZVOyIdLof/ZxJrlpa67DcoylqDLmh+di06tpVCkXyWjH/PAIC/WXA35r/" + "7kmGUilbf2jyWPBbaw84dq+14OyGvRad4T1kRLqIGTb+DB3rWZ+" + "cwMgIZskWGNQSCWYTXs3mHN0VrUqNrjNBVwqzyM0Y09bxubyeBwcWOIp4oa3wvm9is95vgvUS4THuKJ0NJjP1hrcqhVLJPyia2hmtqq092PdoMBLem" + "yCdIaK95P6twe/+47iwnURsbBkWx6oBy+tRcCFHTW/Weie2H/FwrreTMBWvOQzKqeQ1fF0/" + "zUAX01cBzQRyKorHZQmHhEPUoY1HK++VYrR3TgH05KxiSJ+" + "NlMFZlGW8zJT47w7fp8olfMnv5AreCesNwNMz6VHBEVgAdUzjvgQoUqho7kgBOz8kwCHIEg+6DXCom+" + "VeIPCqEnS627km5jy46vhZQJGeMvwxQ8OxLKtDw6SiEMTFstuJPjJfRiePMsC9nI0U9BxhCIDy89LjUbttuCRpvYg4lzVq8lSFOIzPB2jlk5w9ywVf" + "efdKaZBkOfHAycE4n7YzdxyN4EboBQbNcMBNIHblxCSgJ7jFx1b/" + "H1HD4CcTfRTcqPnalHkTaqluO9OzH8GfbP6XDTJlMGyn65BMhnIBZrfZwT0uIIFId/p/" + "vtMo4xsVIKzivp3ZAb1CdsDxzpYCduJWygwAdpHyZLc2nxmpSJ2dv8wjwLZmltmpTrP6GptTCdSl0P5F8+3gu3U/" + "vFjJGAnVuyhototIkOOoYH364DiaMVrGeSMGoZQc3ue6UifdZx8V9VlQywsBP1UqBXgwpUWCruPv8B7QzZnJJHf9cHSWz5xA4TjCymVqQ/" + "J9PSBUYn3+H9USYO9rXU2K8dvzFY8iuIf29w8Kdzlu9NV35vDMQCp2j8vZIoJqFgaydNcCDBOU3su6+" + "1LHhCGYtnZmJ6rfbymVHRiJPnYAL70iOBI0i3iqbCrNhHEqgA87+a0XIqncjJPwDRPYD+" + "uhSSoZEKpFohKVvE68tv0OmSX9UO096qeDemGJf3HyCtRsZ8oSRQBHDccn4JBDEsmq7Dyn8EnbxAm4G5u5GzokrK8tdVdd+" + "I70UpLj2T0COYQLS8enk0rSj4m3Cg2x67EPOPbTDfcJC9zJLSfMrYq5J5JjUWaaLqYzqy37IkSdYiLYhAQz5/" + "aCNaVW0T4aWrgLIadgP2HCeiYcM3ZXztJ9p3hmExBZG4MKyMzW3K7bTGZZxFxlTgPv2t75cMsZtTJyLB4f09cx+" + "P9sRFrU1EnC0iMzgBpvUXYGBsUCjW347TAyCbBQrca3Ntk/" + "ssfDODvQNxWYwDpkAl1SvSwp3XcIHZjQ7yP5i6f07xewXCfOH6oWFJV9z9ZzPdEYvGcMlJJZ75tIHDIvvADOGmpUWRnmMrr/" + "M7Sp7Pd4unBuTk9TtfYj9pvYesEKYWbUm/rXw4qrZ2msaaVKHfYZRqmx99zlipvzD6ejD10rPu46a8VbzVMKDElVkQpCoCmS5a4wMsk1nV8NR3/" + "ZKvMjocpVaFpjKjBziHWWERK8vu7j02FGM7fY+aXro5JdNi5Ikw14QYH9qscorwl9RF9Go/rKm7ax3Xrm/" + "mKsOtd5zqr58A4GnRY+lXx7702r1ejQ4EmJWsZnEULRmUbEYLsCIzgwG49EGATewor3QTs2habI3qSPHZpfbZtk7D4ltqZYq/" + "lLt+z44D+e+iz1sygH1ypyOtfAM0it9ncbAgPKzel2Lylv9yw26dtsxxmT3/dujL1FBDMAMMTrv91RgE+CtPZ/" + "KaXZdqNxmBXzhLQ3bIiFcHPdXAExBYc81/Dj0q2WWfwlUw9xibH+08Ekr5hRFaLk8auTfKfh/V599caOfKc2pw2RmLLjpuI+/ilqM/" + "YFb3RlNB5C2TOxWyQW6cEy5FrQ58AFfm4eAC8K3XuzTH5qLj32GhI4Du2Kwtqp+7k/mZptSJE4bO7pw3JD03kafimrcWF28t9x4TTtyDLUXbdxRJy/" + "q+C/eCIzhcBSwjokFuyD/E+yifABEVQ97jOVFCJWJ2KHU13vajRPyaa/t6COrUideYto4pX6jB8aMBiJ/749UK5Iu9XW1hxVrXs/" + "F8RbPrGzI+0hLE7Wp7Adwrd1kZOs/" + "VshtUIVMUt3fYTRSWGlK9DJ4U7B5+ASMn6OGXibPR+CjPCEOn0AnofaliR0zzf9pao1Mcclx0dBpg0IUjWNDu89De/" + "kccVxzmkh9DapZSq8Z57B3wtZ91T93KPNljzPLTbShwp87xMABC0W/j4VGmiB6/" + "UYYDG6DqEfZ9p41X18kH+dAI4cWFvlWSIEXfZcqo4T4T2sZm3bI9u48v3ESECI3v19ZF9fTWUEiEVCWQ6tZZxquL+" + "KW7aQZB8tYw9PmiB89ifP4bguhdf3y4t+iJZTg2byG3YecCDlcidosjcSXLXz8udeHDxbtVfDEYb7PzO4GYMDIAIevJ3froG3jygY3mHzh/" + "uQor+v1No9+7tCHrbuBLn2JzGbz/rIraN8y228siXpA6NPZ7eXIMZSmsYCBgfFrtxN7TMVAQrKWzBY2HS3O6HrBkGIlskUXzXI/" + "feeBiXUFaKvAxBoIyGKuCbirseFNRjmC6Lq/wh9+/xwjN/aDG/GXr9qzQdNQtT51hILFCg6H4/" + "gRJnwLeWX7j31raA8E4HcSaJldlsYlSVYzh7qLWdhmuSjJo8fK88wQRyTKiR2jykS/PCd1U0OCylFA6YqtwQNknw3HTbDVLGdH9FXHPV14/7DB/" + "JximCbtPRlO6WTo/qls/" + "Rl4vdwvKo4avT2LBALxm21vrFFeNjV97vUgthq+r9Tciemw3QxyOIafJeLue12Bv07MUS1VLklfaAn+XhHj21exq8RiVLL2vgT+" + "CAe4OYwjE3BPHO71L5NCCizIOBShWno6j+ndo0k4Y0FApp6UXsLgv4sQbrRp7eW8rJVlstEWP8nlhbtn5GiQlH2qFLfkdP3Djq46aHXfao0+" + "Q3SbTtw5lZMJoapqO1pcx9C5XQ6vZk64DpqXNWVKgAaVrM7aCNB9a7tcWwU2yVICOtPyDaOPg4874Bhe7sGUh/ONA1GihrnX/ikNYSZ7R+gtfZmZ/" + "AICmbhJ+nBicOMMfOmliJTTvS6Cae3X/Tp/bM6JjT5paEVX/NvDqlOWc7bjvLyIB9vTAUHSYX7bZITgXAnMfjN2ODxA/" + "KE0trGknKA5nRyeddqbSn4z+vouYydv48OHj7EYx9ntEZNVi8qNtEkOVIJaE3HMKxAq2A/kUSrxiFXynB3twj/D76c6AE3blSNgf5/" + "eviDke7vg0wAwbW2NBOT7HKvwdIesPLb00Q1xli78Gr2N6rN/cFxF3k7olYethpJXt30kyPRc56wWR/" + "jPLrWl4lChCf6JiWjjJT0ZYQV1x3hODAnVye7UFout05YlwDvzAw9XmbCUl868Sz12gLfYYD9YLjemZiR7wykpTS2lxaoWMWyZPo4CC3Gi8D3cxBfR" + "1aYk/S4KShnnbUkTfidQ/Eicc2z54i/BUUyeCmPort+siOZOizzsOl7HOMmLQGzB7jVACOEga4bsa4F0FpShXXuqTWvOP/oUlvxlqlKZLb5+1aoN/" + "hlgFwlnKDKh/P9HFAa/OrrkkqIxHOj9YzBZIoWeRna/" + "4aHx5mqlAHIqD9edVVwmSVhHMTl2fvcfDYWiyPbVgK5cKo7Gyw0AQGlBs62xYgEKAT+RLnqhuutHWHyWnlNK2PRnrR43+BwIWYByBG4LL5uituRr/" + "oMPHgJtc6KM0JqT9OeeQt7lksCDtN4mey3SNo5VU1ng3qwytI+t3KLveG9vL4Qx1BNRuP4Kz1UqRrbriZTM7bz7L/" + "HjnbjIGIg9c20pitmqJHKO71x2hA1mZ6PMH/ziDwkfI3kej3F8gqSKazY/" + "twjZrf1dn0DqG+" + "P5nI9oGw6Kf91o7gRclhv7pHqCKGRMwq8Grf6rpW6VlSnOisoKMmd7hE4hcGy7BH5VUTZkGUsZ30meBJgMySM2aty9dCHFbedF87+" + "kWrzWlzaTAZCGmb5Y5b9Afob/9oPbgK5IbDb9NPyhIStJiY5B313bCvrxPDZnqqyLkJ/" + "mAnVGXARrPWxV9x7NfnK07iVUZQ7Eh2x4kwKFJKpuIcG+GKpMPyh3FMI4IP0QJDL9e2sHH7CmtZoxtuCOZcLE8MfQ5/" + "+aYdvfddw+MN84IWiUMdnHWSx9mOjDpZbWCze/CVLgfIgscA1KOyqB83KUV1krfvv7ZpliagXDQBpkBHJR3TdIGynxcoKLMWqGU/" + "AjtjP1CZyFFlw1i0rrLfRICJUjWc156EhBzPh3yvTJOOrJ+mDNmFl/tvGRhvs7tZyWTGgNNejHoxK3HS50fRaaZ1t50jKiyEWIUD3fz7/nEZ/" + "jhcgmYWw7ZNZg45r6D7p4QCbaSv4ug8OjRmojMzDkZ4bD/" + "+Xxd35Der1XB8iacTdNtfu1BioT8P+eNSK9NITrVtbl+CeJkj4hm+gOSdKsMtfR5QpGTmklfZRVm9IR//" + "BZTTu35XJk1Iso+DK9UGXQzDpVSChouFo7Qt34cyNWfgT1mq+vYbPcDIOwrIVb34KiaxO+tP1f0+" + "uARYTyImrJz2IchLlIL4OtgKDalcVfUPS0IcUzvxguC74NMw4ZR0tO+" + "XmB4E5Ob5qNzA291p5ZsIn6blF4661oDE6CcItyEJD8z7fhWKlQnevgjSC5+E7yzTH/" + "OHZBobmNyWF4+Gk39c90YqtUVskfURQ1rjiBSr84Cako5vZKI0VW0zg65Q7vv0qujh6tMqMJDTTChzQTlmZFm+eMI7vSFMb/" + "qte9NHiqajYG5fqcmubCwqdsxQeJMz8Byfi3CqfP0WcoyV9HX8X8VRkF69maJzRHNSmUC/" + "P7WoUW79gAwIeg69OjuQpZrPdybJTNTZ0c5+ijSXGSA+rKNDNB2Xa6uCWkkNtfMjZwB/" + "PaNu2lAlXkuKtEDETuW63wcWTHMMadwVS7uVZ7tOfiOxIbRxfCjp095xqA+9Mot+qF0i4qi0/" + "VUmjaP8xkuFYXmcNESOFKtOINuQxbGk+wX1xxrWvklvIlLOeEY1OO4kOe4VLg9TEf48Pr/FEHnnejXG6ReVHI/" + "uSra6XCzXt4y8i7bPYsNXUlsKDtKkynjclWKWq1CuSGJ42QdRDqrIg3cob1gOVZgjgFFloEqyZ3bzq8QX2XP2Uf9FpFMh6n9E3rJI5TE0DO4lNEOLs" + "dDi11yGFnMPxhI92P3bkSqTiqS1gJvoPZsIIsJanaQMD6WLDY7ajr6j+" + "tIgehqVBPLUgbDEKOKy2acIknat7v3p5tWUob3F2I9HUDDQ2vdvxeDzVC18QssZx1OmxzQ6FYLcdP+" + "YXaw796hGoKcGLjUb68AXMX5d6cwwrgQFMYzDsGxh5Ev2jRe+3fxmQclO1iOOSwhAOrW/oLRLideFyZz5taRjhg7RyEvbSdLY1BJyxJTL+AKk9/m/" + "+QR1RUGQYbUOn4xYD3mTpeBHJ16rXvRVZbQ0T5M097mG7dJpb8TMLl18kSKJLeOdsD3No4UqhHz9kB5oH4iCh4QI84YN8urLb19KblcI7QmRVF4UZ6" + "zZWf02teRFSflUmgXrnQak/psMttftzsU4/" + "8hWqNu6vdDBHnXyJc196LEi7OGY9si68feBHDtAn+wfdPk9OsYMueq0Dhgc0Gt0XOkUO1MhzqZwg00bW/4VOVhO/2cq6YCB6l8r/" + "KPxjHZOBAcob5FcA9bkBRqEDttaIbf4WZPdxGx6nDjULZ30NJkHc8j3CaTG3sLyCxPXk2pnMmjtT8csQzzRniVas4PbQfhPXySsWPCJ+" + "pGefADdEZK1nprxxaOd9r5y6Db41UxuFqf8iqWvsH0tFZ57WJoSgrrMLCO13pH5w2emK2GWaPWH712QRvipEbKIRUnHJqCURLF8pKDOg4gHJcazCcU" + "tLil/EPgYmlVk6jyw5VNbZI5sn8LLSApe1EN+J2v7oMkWEzp0VHLJzKc/AiWLp+c+CUOBlFBU2jO+f/bMzVCgqNJWYGbCGdX2jXha0MBl+W/" + "IteUFsyaqZcyZLE2qQ2HK8677mUNJFSoaPvSTmxpoeHZnjHluuIcWIfWYJ4O8ZlXmMS1krUbZllung93W25Yc0DqE26HLZAT/EcU/" + "iuxXMSVt9mzpOULLmvmQloPIWvn25LZBZaIkAD62a/" + "V+rydpVu58OmXiD6ev5470oBCrES93AOrg+" + "xUVjrpc34gr8SnXcRAGeOTY4GRWvAOxq9gKhQhyF7DseiM7azcXzXKx7sdN05puCIUN13JaDmorKy42Bpd/X/" + "GYXVZyWO0BIJxs35T3U0RdAAxOzALwD0IxazvK/HhXfQhrb2frFmRVmXVzvbbMwx/4B3jnBNCQYIm0Km+hpCymTlBYJQLHMe/" + "RqSfEkbh62ofxy+SRsMYArcGRgltqM0tP0+rmz8cO4pGZXXL0b9viD/BlcIhgvnj/w9EsFB80A8/L3JkyJbNG6SSgTH7viJrTdAv3BE6/" + "xVE0IWDPqoU/NnGc2/NJ7//A5GzhpaVlIQV7MuLZXW+mwjcT8yl36JGef+3l+x8/" + "X5+wwGVmMGwSIWvG5Gatyq7gm0VNOQyWHGVKFysBBgF335vpkK7JUfI7WY7YxpgNYX94DwrSOjNtkORKbY4qfEj4w+" + "iA4cCFQNOZPfk9TBM9MjOTKAF0Ky0MhVE9/" + "72U9RNAM1+rI8mImTJpCZN67JjoBohbuB91rC9LFR2o95i4tgS7uHbABj67rDO+" + "2P11E6su0r0b8zXkHa6sk6DGYlyUPiD7j40uV9jTX3hfsV1D7iLypNpXWVjvDTfhDW92Upp50H8nYb4XyVFHo2BL5KrZHpsHYM4FFJZfcwRX2n0tDj" + "sCVD0jMVKBeom3CtKMtFFkFYfwZE/wKdTEjCdvZ51IDUteDLagSQtdFm03qch/" + "1jJoMneD5hV1EbB1l7EoGY19Sj53Oy+TETDCurHCO1xMT+3yTTdphK/" + "QweO9f7oUhuOW3aqt9lE+mggbHYOo3cHDpSKCTGst2buBJiPupLoXtcV7w5bpRluTIVt/" + "DXUqhNkZ763RJ0CCHX8DP2ARQIU+OL0og8NTB4pdpncOFiUkmhD4YOviNTn2uh9naxuc2YPP8VfASUDRdN+" + "d3MWUj4vtkXdGHKP1P7dy6KonOgss2kCT1eO5NdEEWGjCtMD7CsM8X+YVZAdZU7MGOamtzBGzeZzyP+jdBJ3eLl0Sqi73YkGt68sKSQ3G3Rr0sube+" + "Pud/8LMP9PzEg13AB6Z92cG0V8WmIn8KbOKRXziWNXg4rWNNYW5GE/Zsobxka4RufAhUCsz2JADImveFx0sn+N43Wd/" + "KmzbNQbsimMhmXlad70CXFaRc6CUP1p9vb2zwokW5T1UJ0zUgbDy1BTM6mfnmvWD4weTavswLUXonIxhSvxEP3OJXWDY6lfWYO/" + "hANyGHJRu46lD4m0KHMomR3PLCuo5mg5EGTdSe+FifRhM8P9gvpZ5TyqF4J3GJSlCudp7CmfSuhgPN9fYWIb9B0n04D20sPvP45grRvNj5sg3Jak6+" + "bBhLdqGyNl6ePO0RHUy5woMIVfM3cWvc3WfVwWYj4I+Nb8JafMo/0WO2fe0XjUjZVmzS9I5ZyrxTjBKl5VTQart+qc1hU15LaD4U+/VuuaF7/" + "qWtnUzZjRB4kv/" + "TIaP9mVO2SQfR5aYjqlWRC6hHkvjt1h2WHRqSSZq97EUmBH+" + "IaTQiuvuL9zMIPUCcL4TTLtot0C3e1u04D7Ur7P5GI6xfSw6teaxNcBYiQnVzpL7IHOZWYvvBGZNPznx+" + "JcjzM1q5lmEYr6sZNXuPM8x6wOsL7uzGGc3ej3KJrubXSJ2lxlogDNMyfa9Fgb2y21BlTNPDiSzgVODmNjr6GiTP6Pi+" + "991ivIirGVKe9KHaTq755IJzqscUkFDu/" + "TppL59J6seMaQPrqKs+JwQClvxVbeGdKsoFL2RVOeB5dvnDZ6yaHUq2RtW9QjyNxb7cderCh5ry+n5+5y+d+" + "EUsqnjaa8Rr83nQpgf81lOk3ty4KPSBAtOE+2l8Z4Vmk5GDGS3YBcqCv5/tqUN7rUxJ/ZkAWR4qixomL+u2y2/" + "UOhyK4wvUXxauXseraOFROoZGHyVMkeLoroi3eYZh3qsJqd+uIBlSkor3Cu+RYtNaEDvGhYQRmRnuDo9DbVf5SRt6lzrj/" + "w3cvMUJxlhlStMWUuGyjw4bV306JsOQvZeZa3NhUTnCCC2zBuPQyeUw4xZlD34nzzNIozP1JtU/" + "kFfmCZp50EpLqyVm00JD8V4omUP+k6XFgBTp73thc5Ys2fbXJiE/wL1OQ9yAQ/" + "jcZ0iO9nFHYNHcZpTFzGlb3LZ+V99u9GcIOjxdJCVv+eBMB21ZIvzMHDg9gUI09EvJy0nHLEkGpZ0Uxq/" + "vl6PPAUswq4GcrmjDFP3dqWdqY5ZavZjZiTiRRFYFZL6++fdeCUtQjVbQjI1B/b3BDq3ltaPUKGMm8j5FlwBo8OjUkYEtrUE4Z0QpCiTKAslT9gNh/" + "jZyNwhM8RwcYF5p5TT4oIHkayKac95NuSmH8HHw1dWfJQkRHTfoxMZ5W3LSAU6L4418P5D8rdbVS+" + "QkoiYExjomgDVk8D8EwLYaQhgKOCD15RC6R2KOy/VOSsOLLEaBOdZBeuVcnXFH7wfE4WdprH2N0xaGKpsSX2t7INGwxAwWltbUzmXaN2Q8O3HNo/" + "VVhpLqaoBucAVOgif0C6Q9vDDCXP+PbO2HB6uxudk4bHfkGOpCs1FvBndk8eHdBqBLLpJqicv1uh3Xz8iRpNXun5BH8SpdDJjXQMXNdN/" + "R8nE1MS3Zt4Ec9SVSZLvFG/23y3O1A1lLynTE0Llki+ns3pnby00kR4297Waimb4iBLTu3LLIvSr5TArjGOmDQ3lRRlPZoB87Fg5wFSvd7HHKU/" + "WKAzU4YiCB8Plc8+JJo9oHxugmsyBEnsnJYMLkyNxFzfvtKDaKHyv96e5jgunUjOE5cPl45hlK8OOk+" + "v4bPB8PqanUFPPzPi2PZSZ8aF9ISb0zCnGdXVzkMQ7aapmCWBcN43LnUcDbUHNiPZI3sZsPJnJtX8WEXgzBPNA5hjpKCezPzb+" + "rkMCvLzZIiboYwJzPIxWAZ5tFtMDwmKjyDVQA93krtINXoZF8QOHFYTzBW8zemPGapWPa0coAre9+9KJNMQRssSg+y+" + "mAudtnpYAeOhjgK2kA9wKjQytRhXl2wlR13QoeqhAQBhomlPOpScfly4iXskQFF2A2JTblUAQv5ZGl/" + "LyQB0Dcmd6VRcx3tk888wtL7K3gWKXk21NFI14Zja4/" + "3QH6rInL3Et002Yrujl7A83yZntGZzZ75hfgKh22ZEZFimaaD07dR6xv9iONeU+WkuOFJxnt3ER5XzX2Gp6BFgREujNF9/" + "kx9GP6hn7ZHTmceAesR84VT0Wa2pEVGnACbLAk0YucJXUmWT4yctFJyNUVfGibO53wVLat/iiANFX9u9Hdp5KxBgekgHXrNyx/" + "Msw4S53e73nMckP8BuRALtGIkl5Bgo6f3lMZPENtpku4NTqXFZaeFgjaOy9frPeDDDOinmP6SmdOsi8ekF4jAU9ix4jAYtS+E+Jy9baVQP+kF/" + "gx9MQkFwVuZCpM9ZxgteElStBY13shhfCaeufGIQe0UjUGs5VdnmFxiSwVxME+" + "fwkKxRMazAC9ycUGih0r0R9mpIUC2bzOmezrtLzUccJVyCg1q48ew3xGAfN5whzowPGS/" + "Lz1QHs+" + "mt2QGoDjtpZSSHyMcxJ4zcxcleXUWv9LLTfRkcyqhR3TG2OJYeA1FelKS42lDuaqrMbTET0XC4oUjYzUQAu0HBnJsa8gseM1YI5R85LknFf3rQYg82" + "DO1GJJEUngKeEb8RB6uvCcJ4xs6t5+X7vFEg/" + "1bWl82jRsCji59AFMuDZGBQ+nr3jKTf3wFKtJvPD5bMxPdbYdN9nkyn5yooMLQUtJXhSHgy1SpCoW/UshSbOU+o9/" + "i+Qf3BbhgC36kU0zH8KyPlq0U7a+jJc0BCyuZEUD1FlHBl5Iu5opdPxXB29NzDUIMozrfy+iGJ+e0wZUbkV/" + "25q+7PHiBQs6mV2iSPOxSK2aGKxv4Ti/Pav/4WsO5OyiFt2cPJkmMZaQh8mqvcY+1ewSp9rRiM/" + "yHLSExz0mMN3ENGeQGsSwNjIzuIJfX5SXI7rsRMLJUTKnbWKcNT4hYthBcMkmLbGZph5i35+qiZvhSkaRCrhACNM+MLHkO28TFyRyYvB4Ev8ck+" + "Pj76loJ0CU4JcsvIwdY7adXWEacSNPfP9oWAyMd9OUUoem0duSRK4Nsu1Cs3mQUITE6top4qEs2qG3eregjwDQYqSQWPXjOCop2VN1YlUnsv6d8Uum" + "nFmZkKvFWZBeUTs90VuEe9MWQpkj8kiA16R+s/uKByZP4vFjlBeDwn/RcvxL+K6fALw8e6QTE+80ZzXN/um85Tasidi8AiRS1JtOh+5Z65/" + "gesyNh9GnhONOJz2fNr2QmWJ87VfTmKQqhLI4eSOTDB4dgRaVOAbyfh1slzYQQFwfh5PBqixs/oRj/V3gYfW/" + "kXqcCfINgebEbhef8mqBejMpwzZfF9Blptc1h93Z2oKzESD+gYaxpSnzcVdXN38QY/" + "pBNkgDsok0+qsh7pe2Ansr2bPTcj2wLDkqBwHW3M6xGYowXWB43b8CGbLjDcGG3QQSJ3vjCOK8JqOkci2d+" + "5YihgrYEBcPBRevkOHqxejYHdxRGTWNhNWPDXXqFEyS6iP+7x1tawvC6An702tqDGf3ADMYP5CVSCdH0UcwisOHXjb7aH+PmQFCqn/" + "YYB2yvOwRwj9K1jB9T9n+0ZASF3c71tyS7cmwIfYw+Xmmkg1SbQEXKibnWxtnX2urZlRACJGv8zKCUeK8/" + "N7zUJRwNteIrerYy+giNU2dFtfvwQTK6uk+" + "4jsuh0J8ovZT1AaDKetaFgBJ1xmWCoZZZJ0wuNJbfzOFfnavTlmDJRlqAXPeQ1F6mogTwkSSmPKSjpG/rqDhQ3AwK/" + "hKnYkJafUcyvU0VTIapAgxXM1bPj+atxRfNBaa8h8+emoqPOIbDWYuwt5ObCzjcsyd401t0sUIoy43rBPRVrR+64VP8aafisJrb6x6/" + "kT5MtSSdOTY9n+dK+fbaf1j/EHfEPI2zyZWwQdyRgxo0eqYnuTcNQLOxS0k+DQYD1C557d3ELuKKopZGUyvgWP86oWPX4DwzqK3uqkMilJaeq4/" + "vCNP6+viUSIonmSjTxDZZj/WJR86UOlnWebpYmSZwEklU9tX6ScaaScfzTmjPe/yXgo3/" + "6GIC4XtXl8b7sb6QEbec+8c5+iJXrNhrYu6Bc99S1X7RVwFV9TjqAwRwjmZQtNZiK83/" + "UNj54ZwrHU2W5TSOCJ6Zr24yZzFkeyNvFzSwj4jeFrc0GKjRPHEJwXzAv4w4uvgADD5yRoNLNq/" + "xNLFEsu0Mt8hjLkEIBI1JLXfkfHsKZccjtJGbjRwiI8cFvc1PeKER0qeywNq/tL8Rzx/iXDeuqavrogwfLOnR+aRN8N66LZys/6rAcsSTNfHoXf/" + "7uiJS3rjuzcuWpPpxaPSJV3f8ZL8++duB80E94r4vaBJSHQ946FRyU11aj2x2mnyTl8o8oWndW5UcvFJjvTeBdCYHUTusH9G7FPTG9/" + "efdh1uHnXABe8EwdihKnAAGA5Glws9rbA3qjebj8q17Ade+M+01LIInfq8vpPY6Fn9s8kqqv9lC4rRmgECgAMQ86R9bbymaS/" + "8qKlzh22OGztZl2b77brFtvXxSQ7+u2gV8I8nef0A2SO/7OxxDRb+KiRgstoB5zMIEF7GdTZc5cwF+7Zf3qlsffJKE3/" + "2Pma4ITXT+OecySK3WV2UpkeYwiUduPXxq/LfnWK5IiKYtvBpeT0LVX/" + "9fXrvBcwUjUn6NOCfkwpUj2JjVC5cPeMESncTJoExArTrhTTD+gHSvBXuamgUhmnLR+cXc7VWV+x2Pj5ZvlJcpAO0V1GEya94o4rneF9vJ4kWCKda/" + "z/L3lEguEh8mK4ZuRPgeWiOKg3fQsSKIU2bVW9Kqs/avpAnrsY5Xov4bVrV4KzaLQkp8wZCwgpoAYWAFm5s+rTwuM0TPas8eWsSb/" + "7swP2H3rVlIEtb8wIODDBvWOw3Fn6KoqG+WkjGR1oF3YlqD2x1AswpyH1x29n52YSuKDT7LXIUyNfeX/" + "vRIU6jtq+DxE93u4cngBwDOVzVAQq2j5kflq5+" + "Y0JY3Th7JvFinclWhlQrZWOFmMpQOB9tObkt8fWn5uXVMgUX4QWtI10mw7TqYcvkzpfcz2FJyYN54OAbtefRGmbvGqqPZd+" + "lodOpyMGe7RKKUBJG8LMBtOkdmpsy1tjxsQe3BAydXH4ldwjgyWaw3SRryJIxu736T+wK9AoFLTpE4dxnunRndFzQRzTZy20iTmX+" + "3FD6ENJhfcuFW9E1w1fA982Dy+jh9EEOIe1KJe8TcLW/UIeeUFQYyhqLkEEW3cCj8/" + "CoK49aDvfanO0QhdCAqQf9WH0MsGOJEL5w1OcRvchd9HgXGeyvg3DVM8vQzqCSwC87OU2REEr9iRMSfcxUAVVhcoV0N7UBceYgUsiPqR6pXnH6oPPB" + "AYI9aBYkogPTgb1o0+WFRw9gyx0D6TG0L8h5t54EpMpYm2qVZejvw9+" + "Ey34KOlfTMJaL0wxenVYen0JDgbbeg7KyHA31Gnbz2iLnH2rFE3kPAj0XjW68SNY0B7u2XMJUs1qPFwBhLGEwRwN4Coo5iUo7mNn75yhMNNnZtqoEt" + "5JbV7kExbML4ho24GjazqZxeslDyJJcdb4NCq06FDIHO/1o4eK9Ui/" + "t7+rWD2ChLfTiDr7wV8XgqBqHbTNagFN3Ij7OK49yCsFfPu7U54MMwxRbslqVwsG+" + "Frqokly2o8iTQVGa7JEqJR824PuMbTYhobKR23pJRAEMDr5oImipjO8/" + "pPyeNRTBdx2oVnRc1JDITeDwlJZcrUNojHKuEZTLXx9UjomT1GlgzJYjmxhQrklLiu+" + "LVjSHsIWdElLYFFG6d0JlM292TpgsmOUapwEHNrEogdcaaqPqcyvsKdUNFF2WqgZBo5ZtBeP0blD2StUjwrAJmcdDNFruhyuJZ4Unowg667e20RDp5" + "5c3eyMqZ28mTZ3IbD/g5edMKDtkToX2QIrx1qk/HmBa6dNPMIpf3iFmT1jZx9mjQ0hE8A/" + "w2JW9rFPQQU8eFHXoIt5bDQ0Won66YodBTzHtGAaNxd1h1plysaM2AikXqd8R4NOntJ2mwwk7mGV+RBZ70pTj9mpwJOYGAvFLSFnb4/" + "2x6GQIJyCMYi4R7HgbgJE7ZOXHACNttN1swtJeeKyyHalHcRnp0qxUZnc7jSsPeSO6YdY1PwG3CRf3zAP8o+" + "yU6n11wsweIu39N5MeL4CoCaR7tfnU+25I0Q86M8GIBGyqAdLYJfBD8K9Hp8R+aKCAOaauUCYrm/" + "KSYFGffiVA2ZTWd4Mjgc+DP+BkBHDAQH3ckMVz0RYHlV6N7rKPBAH3vcZaqyVH+ROdSyGeYy8pxXA+ohoYqjKtdnqBXBwCFTnK8+/MNmwIljErYj/" + "mDy+j+" + "dB5QiIpe1tiaDDBrvD0GoQcC1nrBbC7mIrYAI13LhH55KzMUmLR5TpyFQaVsq1oEyhVpW1ReYm24FSViucw6CqxGWkX95LL7llVgamAYRhc6bwpmcm" + "9g0+pcTktPmKRhaiYes4dvkHrVtpO84cCYBchHVReSA2Rx63Rk3Vzmxzon9CN+ghrzLDcqpC+52B+Qj4z0fvJY3rE91VIGDUbtsoO3ajxId3+" + "sjHp5Y0IiCvgZhEeW7BVe+45U/h6mUJk53HBaJhutNBBRklF7Pcl8LWXWkGBZ0IiwgeT9SxbIxK9mFf1jTS5DWhF3pvq11/" + "3r4vTBBTDsu1Uw3IVQjJTe8fmvM+AJc7pD9hFfEoDSbaIWFJ86BIDV3uhWcQPxKiXMq829Lg9s33oni5gicFDceYPOVM/" + "xEGJzz2Gpsx9GQQXpEHiJDFCzIJLbu89mvrAToeetKlbx5Bz36WNZ3tQEp7QZjltZFOWAzcT4ONg+Jr+9aeXqqCYVus/Mx2V0DWg9v0LzLplYGd/" + "J3cevTtWsmXD4nyrhF7xggXzhjC0PxfdDTHvw4kE+SkXHJUE+qGGPaa4ESd1YiccfC7C/" + "N04+EV6hsW4s9IqhnzLwaq3LH9KDXToipOcgsbydKC83tcGC1sdfB30FVAkRZ64Q+" + "8rYyHqQYeizOkMEPsw11rQfm0inegLdbhIcWzW7s1343bu1u8g23GverRCKBL1Yu0Nz31KrMOwn0VkdFC4qn7W3PypUpieztKHGSUHu2q4mp/" + "YMHTIyff/2+XsvXWDeW+HD+CQykMSJreicbirWbRgtlDRyBJ7p288kJdZ8KYJzxtBesu1VZ3m0gk46/" + "LiIIIxlge3Pie+OoVla8uCpc4IQUkhfx6aD5UY1WECRfKrYw0Q+kd1bdm4WinXRWEkG8icUHghPElrYys7FLqqBJ1sRym9fmxCoAnyVm+" + "ay8w0THD9pIK41zMpiIa84kEuIQ9fIaQHPNmKnD+" + "VZokWdm1SbLUr4cxRyPhRrOQHHTGrXsE3o0ahx1alE9QU2q3doijOuGH0Jb0qih19OrdDO03rbS+Y47jEd+" + "eOyQcvDzwWfgsPeO2XbsnOF0ajYYMwodOqn7zSJqazVgd6xOo9y0VgGPyWt0ZsZhmb4h1PwJaO3BDuiZsQod7YMdkadil0rS6R9u6001tHVhwdtXGJ" + "v+UGFOqNySiew7IXiBeQd/LNsCPLi9ij0peDgkBecJRpKNJY9xkeLNl9Mzb7++jY37uUQUUB1ZdPdDKvEEPZP0gXRkC48iaIY+Cn9BaBZD+Topksw/" + "mGyGuYPYng3aS86DBPM7DEiM/Q8sCGhqWCRbY3mMejFEb090/eVAJR8eKp0yKztmcHQlvEvhP9nU4/" + "x7+IDgMUBcTCLYX5lO4FtfY1FkqaWE1h2OPND7YqMX22vqpyw6C5SalKPS7w+RxKlOQIpIBgn/" + "WBfiKftJZaBLwvzyYwkwi6rqx9sdVoA2THGEUapo3XSg4PJMg9xQZ0ebdma6Xai5xRAao3MOUBHzFDp1TtogGydcDGUzINqVv+JRlyMMCs+" + "BDwd1NhSrT91b4+8pZvNsGiNSrtcjZGNBU0KnGRt29b26mASwJ2eHikKnh/" + "Vy7F4Xjl+8tOwfrGEHoZpOyyMDkH7jAv+DgsAls4QmuCX4D6uQhFHLtZCSMMEr4phG/" + "Vl+ijApCRvMBuFWA6768J5YRnwsakj+uvCwEUfECB8A3NhfA3FY8ybv9yJuCTNuGYt81onqqXJM/" + "7hMww7f2ZSJPk7K8HBITJ+eiqIfDQMNoN8mJYpvRORI5/dftZSY/kUQT2/" + "mv9xK7qT660lVUK8XfIkMwHuLWQxY1roKACFuGJoVkax3dHqSEjkpvzFjIVXlFgPuB/" + "jvW2VGaGnaKyTd+ViD4volcecrmmqKYzfsxiReiD1TTdmudYLjfsmqNOH7tBou7s5EFMLJwKWQoW+" + "ZyXOJ2wZerCV6l4uCTsUAUhYEt34ssrxp1eTh2H0L+Uq+6YX1E/" + "EpGZCxz4hOpuwW7GemEf3nF0WI1dQ9GAr2gwUB7xLCAeSo6XM6V5RyU1nrVM6Py1JI6xe1uk3X2zH5R1rrA+" + "eHZFeEAutRhnG1Ptca2QMwrnBPRKusOkF4HL3XdJKIgCXW4WK0+" + "QyF28B9UtPVgarzkEOanc70CTC5kJgx0f0YU2E1gHQtvGZcTj8pUCsOudHqeVQGCWl0DbhMjfZ/4/" + "OoXI3oeOykBetHFDIA37MlGvZ5vpVL7OQwLaPk5qJ4L0NS5ZE0rypA+qrLzD6jhWlRBCkcNJXNi8/" + "mtZC3U5btyQ2M610i7cvgaY1P8CbFv5CS0bBblTBVlQR/" + "vHUmKA+BHGL9xnezUiGdQOP9jut+8rFta35AQz19Cnp5XNlRgreJFgFcuyyz2r0ks3Minfa1shk97zoHE+Ly8r23sIwgJ7NpRvIsg0A+" + "1hpp2rmOhpnWogq+ISx3sZzTgddeP8COZo9c0czbc9lraqfhHL3wOqutD6u89AS204cfM8oTjSIdV+" + "UA3NRpPV9Tsyo8nx10IIoRVMLrlrL5vGyt8pql5cCq2DUxWCfF+MNUPKlDJyb6ANJmZiWCP5VNapsQN1CfFUiDt9w9JKL8crZbRYkzIw4sy/" + "Zx5f57K50Oqb+zqykkFjO0opeHQ2/B2gRl5TEodpj5aB/z91lXE8y1WOxCmyOjrAul718DDr/oqBuTW+HwaJpx15nMZfC+mN4g3KT/5zye/" + "vPoACRcOksgTX2bDXuVx6BL7M0bewTUTa/J3eLSzK1ulhXGb/hUZnSUWUcn4FY3p6c/Axj9kae8AAh4AnOYYyjllzoaCloGZqoFKg3KXl2Z/Egp/" + "uiikZLaGHJK2SaL1zkxP+Dghknt0kEGJp/na/1JmYQVYZZ56kjtEqG38Jx/JTy1p2+a/q/fh6Xf6rvkXqSpEuNlNK0YTDkgHBkmXSTcn+njTzR/" + "HcCS4Wkktg7Fwb7VVpN2mWAMdiU4nnOes0e5/ETvGYh5vFGYKtTMAFr6nGORLVNauds4hb+nTlmQtQQq/ZDkpnSSw1+L/" + "yH4rpNdUsOVxeI+O7iObPbxoxoDAUBBb5s+HjoJ+NwxQGVXQJDVCG3oey3S9iDCThtE3LMlAP/" + "HUgg6GTY1D01o0ITi9ttFBnTJjsMw0sCeJxsMsn6GsAdqy6M3W7T1QsJCbbp3U+UCV/" + "dv1fgLJrz9dgY9ihWQnejzXcpXaqTM8sLAGZKr4Spqy98rMweMrjc0MhC8YKPk48daHp5woc5aJbtNLvNAbiBJDingnTKJYSa9bfZRMsnQdJBs/" + "Ksl1F/Dw9MTOhyvL/sTg7uCJwEcZGN5xoPbxlYbEEXu+yxzt7x0fry+hBrZrM/" + "4PsXkc2jGErOBgjv80Bmm1DeE0djpdHgLfjGwsQH8bTkXy1zNsbfQC3lptfXVQ4HTP1lA5/tv2jGDsU6ygQjdu+7Q2+tobC15ovL+/" + "7l9WfjdVuYp9DgInepSH8ujZKzGdKgDho4LE//9aUgste9TH88dhvAVwoLbDfMGl9I7wXRgGz3Z+3uH9Pmz9frCvI4D0V/" + "kKgJzAcFrmVPRWtJktxdJ8JEUdyORQpjDH7992Hwz2DpUGyI9YFovjziSLO4igHHYSWFD6JS9ePy2n0WFDFymghm/XW2ssm629dd59SGz5gGNaLm/" + "FPj386FpIR6lhZ999R2Fpf79H+KO4cwF8m/" + "EhXWk8irqHcAdQO5ctI7IiAARLkkpJCrS0Xnm7zZncjDmWBbsJXmW4yMCzTTeTZButuh30wuC1PJWlgvdVQASe0oM1jybVfCRlRn0jMRWRcyvNROvo" + "DutinVezcIAX9Ktp7C3d1JSd0uh6VTaD6L3l+ZAKA5by736BeBMZfSVqe2G3j577QqiVDCKT6D3uNA2x3r4cnT8oWdGcuy297H15I83LLp+1/RDo/" + "745K02B3ejdgO6adv3G3DEio9yWEyaIQfFSL12J3Xt4JG0hhD5d3GpjizCzTx+SrBcdZJ/" + "k2PwBeYFKhFTAWQkhDDdyAY0sMlGDQ6Ev1KaT1TFqcOxd22FRaNCj2DLwK70uFsnJAt6vzcVDxrxqI="; + +const char *sqlite_sample_db_v4 = sqlite_sample_db_v4_arr; +const size_t sqlite_sample_db_v4_size = sizeof(sqlite_sample_db_v4_arr) - 1; diff --git a/protocols/Telegram/tdlib/td/test/data.h b/protocols/Telegram/tdlib/td/test/data.h index c447d5cba6..07d863a894 100644 --- a/protocols/Telegram/tdlib/td/test/data.h +++ b/protocols/Telegram/tdlib/td/test/data.h @@ -1,15 +1,24 @@ // -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // #pragma once + #include "td/utils/common.h" -namespace td { + extern const char *thumbnail; extern const size_t thumbnail_size; extern const char *gzip_bomb; extern const size_t gzip_bomb_size; -} // namespace td + +extern const char *gzip; +extern const size_t gzip_size; + +extern const char *sqlite_sample_db_v3; +extern const size_t sqlite_sample_db_v3_size; + +extern const char *sqlite_sample_db_v4; +extern const size_t sqlite_sample_db_v4_size; diff --git a/protocols/Telegram/tdlib/td/test/db.cpp b/protocols/Telegram/tdlib/td/test/db.cpp index 8917dd65b8..61757e9893 100644 --- a/protocols/Telegram/tdlib/td/test/db.cpp +++ b/protocols/Telegram/tdlib/td/test/db.cpp @@ -1,17 +1,29 @@ // -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include "data.h" + #include "td/db/binlog/BinlogHelper.h" +#include "td/db/binlog/ConcurrentBinlog.h" #include "td/db/BinlogKeyValue.h" +#include "td/db/DbKey.h" #include "td/db/SeqKeyValue.h" +#include "td/db/SqliteConnectionSafe.h" +#include "td/db/SqliteDb.h" #include "td/db/SqliteKeyValue.h" #include "td/db/SqliteKeyValueSafe.h" #include "td/db/TsSeqKeyValue.h" +#include "td/actor/actor.h" +#include "td/actor/ConcurrentScheduler.h" + +#include "td/utils/base64.h" #include "td/utils/common.h" +#include "td/utils/filesystem.h" +#include "td/utils/FlatHashMap.h" #include "td/utils/logging.h" #include "td/utils/port/FileFd.h" #include "td/utils/port/thread.h" @@ -24,144 +36,239 @@ #include #include -REGISTER_TESTS(db); - -using namespace td; - template static typename ContainerT::value_type &rand_elem(ContainerT &cont) { - CHECK(0 < cont.size() && cont.size() <= static_cast(std::numeric_limits::max())); - return cont[Random::fast(0, static_cast(cont.size()) - 1)]; + CHECK(0 < cont.size() && cont.size() <= static_cast(std::numeric_limits::max())); + return cont[td::Random::fast(0, static_cast(cont.size()) - 1)]; +} + +TEST(DB, binlog_encryption_bug) { + td::CSlice binlog_name = "test_binlog"; + td::Binlog::destroy(binlog_name).ignore(); + + auto cucumber = td::DbKey::password("cucu'\"mb er"); + auto empty = td::DbKey::empty(); + { + td::Binlog binlog; + binlog + .init( + binlog_name.str(), [&](const td::BinlogEvent &x) {}, cucumber) + .ensure(); + } + { + td::Binlog binlog; + binlog + .init( + binlog_name.str(), [&](const td::BinlogEvent &x) {}, cucumber) + .ensure(); + } } TEST(DB, binlog_encryption) { - CSlice binlog_name = "test_binlog"; - Binlog::destroy(binlog_name).ignore(); + td::CSlice binlog_name = "test_binlog"; + td::Binlog::destroy(binlog_name).ignore(); - auto hello = DbKey::raw_key(std::string(32, 'A')); - auto cucumber = DbKey::password("cucumber"); - auto empty = DbKey::empty(); - auto long_data = string(10000, 'Z'); + auto hello = td::DbKey::raw_key(td::string(32, 'A')); + auto cucumber = td::DbKey::password("cucu'\"mb er"); + auto empty = td::DbKey::empty(); + auto long_data = td::string(10000, 'Z'); { - Binlog binlog; - binlog.init(binlog_name.str(), [](const BinlogEvent &x) {}).ensure(); - binlog.add_raw_event(BinlogEvent::create_raw(binlog.next_id(), 1, 0, create_storer("AAAA"))); - binlog.add_raw_event(BinlogEvent::create_raw(binlog.next_id(), 1, 0, create_storer("BBBB"))); - binlog.add_raw_event(BinlogEvent::create_raw(binlog.next_id(), 1, 0, create_storer(long_data))); + td::Binlog binlog; + binlog.init(binlog_name.str(), [](const td::BinlogEvent &x) {}).ensure(); + binlog.add_raw_event(td::BinlogEvent::create_raw(binlog.next_id(), 1, 0, td::create_storer("AAAA")), + td::BinlogDebugInfo{__FILE__, __LINE__}); + binlog.add_raw_event(td::BinlogEvent::create_raw(binlog.next_id(), 1, 0, td::create_storer("BBBB")), + td::BinlogDebugInfo{__FILE__, __LINE__}); + binlog.add_raw_event(td::BinlogEvent::create_raw(binlog.next_id(), 1, 0, td::create_storer(long_data)), + td::BinlogDebugInfo{__FILE__, __LINE__}); LOG(INFO) << "SET PASSWORD"; binlog.change_key(cucumber); binlog.change_key(hello); LOG(INFO) << "OK"; - binlog.add_raw_event(BinlogEvent::create_raw(binlog.next_id(), 1, 0, create_storer("CCCC"))); + binlog.add_raw_event(td::BinlogEvent::create_raw(binlog.next_id(), 1, 0, td::create_storer("CCCC")), + td::BinlogDebugInfo{__FILE__, __LINE__}); binlog.close().ensure(); } + return; + auto add_suffix = [&] { - auto fd = FileFd::open(binlog_name, FileFd::Flags::Write | FileFd::Flags::Append).move_as_ok(); + auto fd = td::FileFd::open(binlog_name, td::FileFd::Flags::Write | td::FileFd::Flags::Append).move_as_ok(); fd.write("abacabadaba").ensure(); }; add_suffix(); { - std::vector v; + td::vector v; LOG(INFO) << "RESTART"; - Binlog binlog; - binlog.init(binlog_name.str(), [&](const BinlogEvent &x) { v.push_back(x.data_.str()); }, hello).ensure(); - CHECK(v == std::vector({"AAAA", "BBBB", long_data, "CCCC"})); + td::Binlog binlog; + binlog + .init( + binlog_name.str(), [&](const td::BinlogEvent &x) { v.push_back(x.data_.str()); }, hello) + .ensure(); + CHECK(v == td::vector({"AAAA", "BBBB", long_data, "CCCC"})); } add_suffix(); { - std::vector v; + td::vector v; LOG(INFO) << "RESTART"; - Binlog binlog; - auto status = binlog.init(binlog_name.str(), [&](const BinlogEvent &x) { v.push_back(x.data_.str()); }, cucumber); + td::Binlog binlog; + auto status = binlog.init( + binlog_name.str(), [&](const td::BinlogEvent &x) { v.push_back(x.data_.str()); }, cucumber); CHECK(status.is_error()); } add_suffix(); { - std::vector v; + td::vector v; LOG(INFO) << "RESTART"; - Binlog binlog; - auto status = - binlog.init(binlog_name.str(), [&](const BinlogEvent &x) { v.push_back(x.data_.str()); }, cucumber, hello); - CHECK(v == std::vector({"AAAA", "BBBB", long_data, "CCCC"})); + td::Binlog binlog; + auto status = binlog.init( + binlog_name.str(), [&](const td::BinlogEvent &x) { v.push_back(x.data_.str()); }, cucumber, hello); + CHECK(v == td::vector({"AAAA", "BBBB", long_data, "CCCC"})); } -}; +} TEST(DB, sqlite_lfs) { - string path = "test_sqlite_db"; - SqliteDb::destroy(path).ignore(); - SqliteDb db; - db.init(path).ensure(); + td::string path = "test_sqlite_db"; + td::SqliteDb::destroy(path).ignore(); + auto db = td::SqliteDb::open_with_key(path, true, td::DbKey::empty()).move_as_ok(); db.exec("PRAGMA journal_mode=WAL").ensure(); db.exec("PRAGMA user_version").ensure(); + td::SqliteDb::destroy(path).ignore(); } TEST(DB, sqlite_encryption) { - string path = "test_sqlite_db"; - SqliteDb::destroy(path).ignore(); + td::string path = "test_sqlite_db"; + td::SqliteDb::destroy(path).ignore(); - auto empty = DbKey::empty(); - auto cucumber = DbKey::password("cucumber"); - auto tomato = DbKey::raw_key(string(32, 'a')); + auto empty = td::DbKey::empty(); + auto cucumber = td::DbKey::password("cucu'\"mb er"); + auto tomato = td::DbKey::raw_key(td::string(32, 'a')); { - auto db = SqliteDb::open_with_key(path, empty).move_as_ok(); - db.set_user_version(123); - auto kv = SqliteKeyValue(); - kv.init_with_connection(db.clone(), "kv"); + auto db = td::SqliteDb::open_with_key(path, true, empty).move_as_ok(); + db.set_user_version(123).ensure(); + auto kv = td::SqliteKeyValue(); + kv.init_with_connection(db.clone(), "kv").ensure(); kv.set("a", "b"); } - SqliteDb::open_with_key(path, cucumber).ensure_error(); // key was set... + td::SqliteDb::open_with_key(path, false, cucumber).ensure_error(); - SqliteDb::change_key(path, cucumber, empty).ensure(); + td::SqliteDb::change_key(path, false, cucumber, empty).ensure(); + td::SqliteDb::change_key(path, false, cucumber, empty).ensure(); - SqliteDb::open_with_key(path, tomato).ensure_error(); + td::SqliteDb::open_with_key(path, false, tomato).ensure_error(); { - auto db = SqliteDb::open_with_key(path, cucumber).move_as_ok(); - auto kv = SqliteKeyValue(); - kv.init_with_connection(db.clone(), "kv"); + auto db = td::SqliteDb::open_with_key(path, false, cucumber).move_as_ok(); + auto kv = td::SqliteKeyValue(); + kv.init_with_connection(db.clone(), "kv").ensure(); CHECK(kv.get("a") == "b"); CHECK(db.user_version().ok() == 123); } - SqliteDb::change_key(path, tomato, cucumber).ensure(); - SqliteDb::change_key(path, tomato, cucumber).ensure(); + td::SqliteDb::change_key(path, false, tomato, cucumber).ensure(); + td::SqliteDb::change_key(path, false, tomato, cucumber).ensure(); - SqliteDb::open_with_key(path, cucumber).ensure_error(); + td::SqliteDb::open_with_key(path, false, cucumber).ensure_error(); { - auto db = SqliteDb::open_with_key(path, tomato).move_as_ok(); - auto kv = SqliteKeyValue(); - kv.init_with_connection(db.clone(), "kv"); + auto db = td::SqliteDb::open_with_key(path, false, tomato).move_as_ok(); + auto kv = td::SqliteKeyValue(); + kv.init_with_connection(db.clone(), "kv").ensure(); CHECK(kv.get("a") == "b"); CHECK(db.user_version().ok() == 123); } - SqliteDb::change_key(path, empty, tomato).ensure(); - SqliteDb::change_key(path, empty, tomato).ensure(); + td::SqliteDb::change_key(path, false, empty, tomato).ensure(); + td::SqliteDb::change_key(path, false, empty, tomato).ensure(); { - auto db = SqliteDb::open_with_key(path, empty).move_as_ok(); - auto kv = SqliteKeyValue(); - kv.init_with_connection(db.clone(), "kv"); + auto db = td::SqliteDb::open_with_key(path, false, empty).move_as_ok(); + auto kv = td::SqliteKeyValue(); + kv.init_with_connection(db.clone(), "kv").ensure(); CHECK(kv.get("a") == "b"); CHECK(db.user_version().ok() == 123); } - SqliteDb::open_with_key(path, cucumber).ensure_error(); + td::SqliteDb::open_with_key(path, false, cucumber).ensure_error(); + td::SqliteDb::destroy(path).ignore(); +} + +TEST(DB, sqlite_encryption_migrate_v3) { + td::string path = "test_sqlite_db"; + td::SqliteDb::destroy(path).ignore(); + auto cucumber = td::DbKey::password("cucumber"); + auto empty = td::DbKey::empty(); + if (false) { + // sqlite_sample_db was generated by the following code using SQLCipher based on SQLite 3.15.2 + { + auto db = td::SqliteDb::change_key(path, true, cucumber, empty).move_as_ok(); + db.set_user_version(123).ensure(); + auto kv = td::SqliteKeyValue(); + kv.init_with_connection(db.clone(), "kv").ensure(); + kv.set("hello", "world"); + } + LOG(ERROR) << td::base64_encode(td::read_file(path).move_as_ok()); + } + td::write_file(path, td::base64_decode(td::Slice(sqlite_sample_db_v3, sqlite_sample_db_v3_size)).move_as_ok()) + .ensure(); + { + auto db = td::SqliteDb::open_with_key(path, true, cucumber).move_as_ok(); + auto kv = td::SqliteKeyValue(); + kv.init_with_connection(db.clone(), "kv").ensure(); + CHECK(kv.get("hello") == "world"); + CHECK(db.user_version().ok() == 123); + } + td::SqliteDb::destroy(path).ignore(); +} + +TEST(DB, sqlite_encryption_migrate_v4) { + td::string path = "test_sqlite_db"; + td::SqliteDb::destroy(path).ignore(); + auto cucumber = td::DbKey::password("cucu'\"mb er"); + auto empty = td::DbKey::empty(); + if (false) { + // sqlite_sample_db was generated by the following code using SQLCipher 4.4.0 + { + auto db = td::SqliteDb::change_key(path, true, cucumber, empty).move_as_ok(); + db.set_user_version(123).ensure(); + auto kv = td::SqliteKeyValue(); + kv.init_with_connection(db.clone(), "kv").ensure(); + kv.set("hello", "world"); + } + LOG(ERROR) << td::base64_encode(td::read_file(path).move_as_ok()); + } + td::write_file(path, td::base64_decode(td::Slice(sqlite_sample_db_v4, sqlite_sample_db_v4_size)).move_as_ok()) + .ensure(); + { + auto r_db = td::SqliteDb::open_with_key(path, true, cucumber); + if (r_db.is_error()) { + LOG(ERROR) << r_db.error(); + return; + } + auto db = r_db.move_as_ok(); + auto kv = td::SqliteKeyValue(); + auto status = kv.init_with_connection(db.clone(), "kv"); + if (status.is_error()) { + LOG(ERROR) << status; + } else { + CHECK(kv.get("hello") == "world"); + CHECK(db.user_version().ok() == 123); + } + } + td::SqliteDb::destroy(path).ignore(); } -using SeqNo = uint64; +using SeqNo = td::uint64; struct DbQuery { - enum Type { Get, Set, Erase } type; + enum class Type { Get, Set, Erase } type = Type::Get; SeqNo tid = 0; - int32 id = 0; - string key; - string value; + td::int32 id = 0; + td::string key; + td::string value; }; template @@ -172,13 +279,39 @@ class QueryHandler { } void do_query(DbQuery &query) { switch (query.type) { - case DbQuery::Get: + case DbQuery::Type::Get: + query.value = impl_.get(query.key); + return; + case DbQuery::Type::Set: + impl_.set(query.key, query.value); + query.tid = 1; + return; + case DbQuery::Type::Erase: + impl_.erase(query.key); + query.tid = 1; + return; + } + } + + private: + ImplT impl_; +}; + +template +class SeqQueryHandler { + public: + ImplT &impl() { + return impl_; + } + void do_query(DbQuery &query) { + switch (query.type) { + case DbQuery::Type::Get: query.value = impl_.get(query.key); return; - case DbQuery::Set: + case DbQuery::Type::Set: query.tid = impl_.set(query.key, query.value); return; - case DbQuery::Erase: + case DbQuery::Type::Erase: query.tid = impl_.erase(query.key); return; } @@ -190,93 +323,93 @@ class QueryHandler { class SqliteKV { public: - string get(string key) { + td::string get(const td::string &key) { return kv_->get().get(key); } - SeqNo set(string key, string value) { + SeqNo set(const td::string &key, const td::string &value) { kv_->get().set(key, value); return 0; } - SeqNo erase(string key) { + SeqNo erase(const td::string &key) { kv_->get().erase(key); return 0; } - Status init(string name) { - auto sql_connection = std::make_shared(name); - kv_ = std::make_shared("kv", sql_connection); - return Status::OK(); + td::Status init(const td::string &name) { + auto sql_connection = std::make_shared(name, td::DbKey::empty()); + kv_ = std::make_shared("kv", sql_connection); + return td::Status::OK(); } void close() { kv_.reset(); } private: - std::shared_ptr kv_; + std::shared_ptr kv_; }; class BaselineKV { public: - string get(string key) { + td::string get(const td::string &key) { return map_[key]; } - SeqNo set(string key, string value) { - map_[key] = value; + SeqNo set(const td::string &key, td::string value) { + map_[key] = std::move(value); return ++current_tid_; } - SeqNo erase(string key) { + SeqNo erase(const td::string &key) { map_.erase(key); return ++current_tid_; } private: - std::map map_; + std::map map_; SeqNo current_tid_ = 0; }; TEST(DB, key_value) { - SET_VERBOSITY_LEVEL(VERBOSITY_NAME(INFO)); - std::vector keys; - std::vector values; + td::vector keys; + td::vector values; for (int i = 0; i < 100; i++) { - keys.push_back(rand_string('a', 'b', Random::fast(1, 10))); + keys.push_back(td::rand_string('a', 'b', td::Random::fast(1, 10))); } - for (int i = 0; i < 1000; i++) { - values.push_back(rand_string('a', 'b', Random::fast(1, 10))); + for (int i = 0; i < 10; i++) { + values.push_back(td::rand_string('a', 'b', td::Random::fast(1, 10))); } - int queries_n = 300000; - std::vector queries(queries_n); + int queries_n = 1000; + td::vector queries(queries_n); for (auto &q : queries) { - int op = Random::fast(0, 2); + int op = td::Random::fast(0, 2); const auto &key = rand_elem(keys); const auto &value = rand_elem(values); if (op == 0) { - q.type = DbQuery::Get; + q.type = DbQuery::Type::Get; q.key = key; } else if (op == 1) { - q.type = DbQuery::Erase; + q.type = DbQuery::Type::Erase; q.key = key; } else if (op == 2) { - q.type = DbQuery::Set; + q.type = DbQuery::Type::Set; q.key = key; q.value = value; } } QueryHandler baseline; - QueryHandler kv; - QueryHandler ts_kv; - QueryHandler> new_kv; + QueryHandler kv; + QueryHandler ts_kv; + QueryHandler> new_kv; - CSlice new_kv_name = "test_new_kv"; - Binlog::destroy(new_kv_name).ignore(); + td::CSlice new_kv_name = "test_new_kv"; + td::Binlog::destroy(new_kv_name).ignore(); new_kv.impl().init(new_kv_name.str()).ensure(); - QueryHandler sqlite_kv; - CSlice name = "test_sqlite_kv"; - SqliteDb::destroy(name).ignore(); - sqlite_kv.impl().init(name.str()).ensure(); + QueryHandler sqlite_kv; + td::CSlice path = "test_sqlite_kv"; + td::SqliteDb::destroy(path).ignore(); + auto db = td::SqliteDb::open_with_key(path, true, td::DbKey::empty()).move_as_ok(); + sqlite_kv.impl().init_with_connection(std::move(db), "KV").ensure(); int cnt = 0; for (auto &q : queries) { @@ -294,41 +427,82 @@ TEST(DB, key_value) { ASSERT_EQ(a.value, c.value); ASSERT_EQ(a.value, d.value); ASSERT_EQ(a.value, e.value); - if (cnt++ % 10000 == 0) { + if (cnt++ % 200 == 0) { new_kv.impl().init(new_kv_name.str()).ensure(); } } + td::SqliteDb::destroy(path).ignore(); + td::Binlog::destroy(new_kv_name).ignore(); +} + +TEST(DB, key_value_set_all) { + td::vector keys; + td::vector values; + + for (int i = 0; i < 100; i++) { + keys.push_back(td::rand_string('a', 'b', td::Random::fast(1, 10))); + } + for (int i = 0; i < 10; i++) { + values.push_back(td::rand_string('a', 'b', td::Random::fast(1, 10))); + } + + td::SqliteKeyValue sqlite_kv; + td::CSlice sqlite_kv_name = "test_sqlite_kv"; + td::SqliteDb::destroy(sqlite_kv_name).ignore(); + auto db = td::SqliteDb::open_with_key(sqlite_kv_name, true, td::DbKey::empty()).move_as_ok(); + sqlite_kv.init_with_connection(std::move(db), "KV").ensure(); + + BaselineKV kv; + + int queries_n = 100; + while (queries_n-- > 0) { + int cnt = td::Random::fast(0, 10); + td::FlatHashMap key_values; + for (int i = 0; i < cnt; i++) { + auto key = rand_elem(keys); + auto value = rand_elem(values); + key_values[key] = value; + kv.set(key, value); + } + + sqlite_kv.set_all(key_values); + + for (auto &key : keys) { + CHECK(kv.get(key) == sqlite_kv.get(key)); + } + } + td::SqliteDb::destroy(sqlite_kv_name).ignore(); } -TEST(DB, thread_key_value) { #if !TD_THREAD_UNSUPPORTED - std::vector keys; - std::vector values; +TEST(DB, thread_key_value) { + td::vector keys; + td::vector values; for (int i = 0; i < 100; i++) { - keys.push_back(rand_string('a', 'b', Random::fast(1, 10))); + keys.push_back(td::rand_string('a', 'b', td::Random::fast(1, 10))); } for (int i = 0; i < 1000; i++) { - values.push_back(rand_string('a', 'b', Random::fast(1, 10))); + values.push_back(td::rand_string('a', 'b', td::Random::fast(1, 10))); } int threads_n = 4; - int queries_n = 100000; + int queries_n = 10000; - std::vector> queries(threads_n, std::vector(queries_n)); + td::vector> queries(threads_n, td::vector(queries_n)); for (auto &qs : queries) { for (auto &q : qs) { - int op = Random::fast(0, 10); + int op = td::Random::fast(0, 10); const auto &key = rand_elem(keys); const auto &value = rand_elem(values); if (op > 1) { - q.type = DbQuery::Get; + q.type = DbQuery::Type::Get; q.key = key; } else if (op == 0) { - q.type = DbQuery::Erase; + q.type = DbQuery::Type::Erase; q.key = key; } else if (op == 1) { - q.type = DbQuery::Set; + q.type = DbQuery::Type::Set; q.key = key; q.value = value; } @@ -336,12 +510,12 @@ TEST(DB, thread_key_value) { } QueryHandler baseline; - QueryHandler ts_kv; + SeqQueryHandler ts_kv; - std::vector threads(threads_n); - std::vector> res(threads_n); + td::vector threads(threads_n); + td::vector> res(threads_n); for (int i = 0; i < threads_n; i++) { - threads[i] = thread([&ts_kv, &queries, &res, i]() { + threads[i] = td::thread([&ts_kv, &queries, &res, i] { for (auto q : queries[i]) { ts_kv.do_query(q); res[i].push_back(q); @@ -352,7 +526,7 @@ TEST(DB, thread_key_value) { thread.join(); } - std::vector pos(threads_n); + td::vector pos(threads_n); while (true) { bool was = false; for (int i = 0; i < threads_n; i++) { @@ -362,7 +536,7 @@ TEST(DB, thread_key_value) { } auto &q = res[i][p]; if (q.tid == 0) { - if (q.type == DbQuery::Get) { + if (q.type == DbQuery::Type::Get) { auto nq = q; baseline.do_query(nq); if (nq.value == q.value) { @@ -402,25 +576,23 @@ TEST(DB, thread_key_value) { baseline.do_query(res[best][pos[best]]); pos[best]++; } -#endif } +#endif TEST(DB, persistent_key_value) { - using KeyValue = BinlogKeyValue; - // using KeyValue = PersistentKeyValue; - // using KeyValue = SqliteKV; - SET_VERBOSITY_LEVEL(VERBOSITY_NAME(WARNING)); - std::vector keys; - std::vector values; - CSlice name = "test_pmc"; - Binlog::destroy(name).ignore(); - SqliteDb::destroy(name).ignore(); + using KeyValue = td::BinlogKeyValue; + // using KeyValue = td::SqliteKeyValue; + td::vector keys; + td::vector values; + td::CSlice path = "test_pmc"; + td::Binlog::destroy(path).ignore(); + td::SqliteDb::destroy(path).ignore(); for (int i = 0; i < 100; i++) { - keys.push_back(rand_string('a', 'b', Random::fast(1, 10))); + keys.push_back(td::rand_string('a', 'b', td::Random::fast(1, 10))); } for (int i = 0; i < 1000; i++) { - values.push_back(rand_string('a', 'b', Random::fast(1, 10))); + values.push_back(td::rand_string('a', 'b', td::Random::fast(1, 10))); } QueryHandler baseline; @@ -429,34 +601,34 @@ TEST(DB, persistent_key_value) { int threads_n = 4; int queries_n = 3000 / threads_n; - std::vector> queries(threads_n, std::vector(queries_n)); + td::vector> queries(threads_n, td::vector(queries_n)); for (auto &qs : queries) { for (auto &q : qs) { - int op = Random::fast(0, 10); + int op = td::Random::fast(0, 10); const auto &key = rand_elem(keys); const auto &value = rand_elem(values); if (op > 1) { - q.type = DbQuery::Get; + q.type = DbQuery::Type::Get; q.key = key; } else if (op == 0) { - q.type = DbQuery::Erase; + q.type = DbQuery::Type::Erase; q.key = key; } else if (op == 1) { - q.type = DbQuery::Set; + q.type = DbQuery::Type::Set; q.key = key; q.value = value; } } } - std::vector> res(threads_n); - class Worker : public Actor { + td::vector> res(threads_n); + class Worker final : public td::Actor { public: - Worker(ActorShared<> parent, std::shared_ptr> kv, const std::vector *queries, - std::vector *res) + Worker(td::ActorShared<> parent, std::shared_ptr> kv, + const td::vector *queries, td::vector *res) : parent_(std::move(parent)), kv_(std::move(kv)), queries_(queries), res_(res) { } - void loop() override { + void loop() final { for (auto q : *queries_) { kv_->do_query(q); res_->push_back(q); @@ -465,55 +637,54 @@ TEST(DB, persistent_key_value) { } private: - ActorShared<> parent_; - std::shared_ptr> kv_; - const std::vector *queries_; - std::vector *res_; + td::ActorShared<> parent_; + std::shared_ptr> kv_; + const td::vector *queries_; + td::vector *res_; }; - class Main : public Actor { + class Main final : public td::Actor { public: - Main(int threads_n, const std::vector> *queries, std::vector> *res) - : threads_n_(threads_n), queries_(queries), res_(res) { + Main(int threads_n, const td::vector> *queries, td::vector> *res) + : threads_n_(threads_n), queries_(queries), res_(res), ref_cnt_(threads_n) { } - void start_up() override { - LOG(INFO) << "start_up"; + void start_up() final { + LOG(INFO) << "Start up"; kv_->impl().init("test_pmc").ensure(); - ref_cnt_ = threads_n_; for (int i = 0; i < threads_n_; i++) { - create_actor_on_scheduler("Worker", i + 1, actor_shared(this, 2), kv_, &queries_->at(i), &res_->at(i)) + td::create_actor_on_scheduler("Worker", i + 1, actor_shared(this, 2), kv_, &queries_->at(i), + &res_->at(i)) .release(); } } - void tear_down() override { - LOG(INFO) << "tear_down"; + void tear_down() final { + LOG(INFO) << "Tear down"; // kv_->impl().close(); } - void hangup_shared() override { - LOG(INFO) << "hangup"; + void hangup_shared() final { + LOG(INFO) << "Hang up"; ref_cnt_--; if (ref_cnt_ == 0) { kv_->impl().close(); - Scheduler::instance()->finish(); + td::Scheduler::instance()->finish(); stop(); } } - void hangup() override { + void hangup() final { LOG(ERROR) << "BAD HANGUP"; } private: int threads_n_; - const std::vector> *queries_; - std::vector> *res_; + const td::vector> *queries_; + td::vector> *res_; - std::shared_ptr> kv_{new QueryHandler()}; + std::shared_ptr> kv_{new SeqQueryHandler()}; int ref_cnt_; }; - ConcurrentScheduler sched; - sched.init(threads_n); + td::ConcurrentScheduler sched(threads_n, 0); sched.create_actor_unsafe
(0, "Main", threads_n, &queries, &res).release(); sched.start(); while (sched.run_main(10)) { @@ -521,7 +692,7 @@ TEST(DB, persistent_key_value) { } sched.finish(); - std::vector pos(threads_n); + td::vector pos(threads_n); while (true) { bool was = false; for (int i = 0; i < threads_n; i++) { @@ -531,7 +702,7 @@ TEST(DB, persistent_key_value) { } auto &q = res[i][p]; if (q.tid == 0) { - if (q.type == DbQuery::Get) { + if (q.type == DbQuery::Type::Get) { auto nq = q; baseline.do_query(nq); if (nq.value == q.value) { @@ -572,4 +743,5 @@ TEST(DB, persistent_key_value) { pos[best]++; } } + td::SqliteDb::destroy(path).ignore(); } diff --git a/protocols/Telegram/tdlib/td/test/fuzz_url.cpp b/protocols/Telegram/tdlib/td/test/fuzz_url.cpp index 74047135c0..0a1e880cbb 100644 --- a/protocols/Telegram/tdlib/td/test/fuzz_url.cpp +++ b/protocols/Telegram/tdlib/td/test/fuzz_url.cpp @@ -1,5 +1,5 @@ // -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -17,7 +17,7 @@ static td::string get_utf_string(td::Slice from) { td::string res; td::string alph = " ab@./01#"; for (auto c : from) { - res += alph[td::uint8(c) % alph.size()]; + res += alph[static_cast(c) % alph.size()]; } LOG(ERROR) << res; return res; diff --git a/protocols/Telegram/tdlib/td/test/http.cpp b/protocols/Telegram/tdlib/td/test/http.cpp index 98c94b2e8a..2498c53e5c 100644 --- a/protocols/Telegram/tdlib/td/test/http.cpp +++ b/protocols/Telegram/tdlib/td/test/http.cpp @@ -1,10 +1,14 @@ // -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include "td/utils/tests.h" +#include "data.h" + +#if TD_DARWIN_WATCH_OS +#include "td/net/DarwinHttp.h" +#endif #include "td/net/HttpChunkedByteFlow.h" #include "td/net/HttpHeaderCreator.h" @@ -12,39 +16,40 @@ #include "td/net/HttpReader.h" #include "td/utils/AesCtrByteFlow.h" +#include "td/utils/algorithm.h" #include "td/utils/base64.h" #include "td/utils/buffer.h" #include "td/utils/BufferedFd.h" #include "td/utils/ByteFlow.h" +#include "td/utils/common.h" #include "td/utils/crypto.h" #include "td/utils/format.h" #include "td/utils/Gzip.h" #include "td/utils/GzipByteFlow.h" #include "td/utils/logging.h" #include "td/utils/misc.h" -#include "td/utils/port/Fd.h" +#include "td/utils/port/detail/PollableFd.h" #include "td/utils/port/FileFd.h" #include "td/utils/port/path.h" +#include "td/utils/port/PollFlags.h" #include "td/utils/port/thread_local.h" #include "td/utils/Random.h" #include "td/utils/Slice.h" +#include "td/utils/SliceBuilder.h" #include "td/utils/Status.h" - -#include "test/data.h" +#include "td/utils/tests.h" +#include "td/utils/UInt.h" #include -#include +#include #include +#include -REGISTER_TESTS(http) - -using namespace td; - -static string make_chunked(string str) { - auto v = rand_split(str); - string res; +static td::string make_chunked(const td::string &str) { + auto v = td::rand_split(str); + td::string res; for (auto &s : v) { - res += PSTRING() << format::as_hex_dump(int(s.size())); + res += PSTRING() << td::format::as_hex_dump(static_cast(s.size())); res += "\r\n"; res += s; res += "\r\n"; @@ -53,30 +58,33 @@ static string make_chunked(string str) { return res; } -static string gen_http_content() { - int t = Random::fast(0, 2); +static td::string gen_http_content() { + int t = td::Random::fast(0, 2); int len; if (t == 0) { - len = Random::fast(1, 10); + len = td::Random::fast(1, 10); } else if (t == 1) { - len = Random::fast(100, 200); + len = td::Random::fast(100, 200); } else { - len = Random::fast(1000, 20000); + len = td::Random::fast(1000, 20000); } - return rand_string(std::numeric_limits::min(), std::numeric_limits::max(), len); + return td::rand_string(std::numeric_limits::min(), std::numeric_limits::max(), len); } -static string make_http_query(string content, bool is_chunked, bool is_gzip, double gzip_k = 5, - string zip_override = "") { - HttpHeaderCreator hc; +static td::string make_http_query(td::string content, bool is_json, bool is_chunked, bool is_gzip, double gzip_k = 5, + td::string zip_override = td::string()) { + td::HttpHeaderCreator hc; hc.init_post("/"); - hc.add_header("jfkdlsahhjk", rand_string('a', 'z', Random::fast(1, 2000))); + hc.add_header("jfkdlsahhjk", td::rand_string('a', 'z', td::Random::fast(1, 2000))); + if (is_json) { + hc.add_header("content-type", "application/json"); + } if (is_gzip) { - BufferSlice zip; + td::BufferSlice zip; if (zip_override.empty()) { - zip = gzencode(content, gzip_k); + zip = td::gzencode(content, gzip_k); } else { - zip = BufferSlice(zip_override); + zip = td::BufferSlice(zip_override); } if (!zip.empty()) { hc.add_header("content-encoding", "gzip"); @@ -89,22 +97,19 @@ static string make_http_query(string content, bool is_chunked, bool is_gzip, dou } else { hc.set_content_size(content.size()); } - string res; auto r_header = hc.finish(); CHECK(r_header.is_ok()); - res += r_header.ok().str(); - res += content; - return res; + return PSTRING() << r_header.ok() << content; } -static string rand_http_query(string content) { - bool is_chunked = Random::fast(0, 1) == 0; - bool is_gzip = Random::fast(0, 1) == 0; - return make_http_query(std::move(content), is_chunked, is_gzip); +static td::string rand_http_query(td::string content) { + bool is_chunked = td::Random::fast_bool(); + bool is_gzip = td::Random::fast_bool(); + return make_http_query(std::move(content), false, is_chunked, is_gzip); } -static string join(const std::vector &v) { - string res; +static td::string join(const td::vector &v) { + td::string res; for (auto &s : v) { res += s; } @@ -112,10 +117,10 @@ static string join(const std::vector &v) { } TEST(Http, stack_overflow) { - ChainBufferWriter writer; - BufferSlice slice(string(256, 'A')); + td::ChainBufferWriter writer; + td::BufferSlice slice(td::string(256, 'A')); for (int i = 0; i < 1000000; i++) { - ChainBufferWriter tmp_writer; + td::ChainBufferWriter tmp_writer; writer.append(slice.clone()); } { @@ -128,30 +133,50 @@ TEST(Http, reader) { #if TD_ANDROID || TD_TIZEN return; #endif - clear_thread_locals(); - SET_VERBOSITY_LEVEL(VERBOSITY_NAME(INFO)); - auto start_mem = BufferAllocator::get_buffer_mem(); + td::clear_thread_locals(); + auto start_mem = td::BufferAllocator::get_buffer_mem(); + auto start_size = td::BufferAllocator::get_buffer_slice_size(); { - auto input_writer = ChainBufferWriter::create_empty(); + td::BufferSlice a("test test"); + td::BufferSlice b = std::move(a); +#if TD_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wunknown-warning-option" +#pragma clang diagnostic ignored "-Wself-move" +#endif + a = std::move(a); + b = std::move(b); +#if TD_CLANG +#pragma clang diagnostic pop +#endif + a = std::move(b); + td::BufferSlice c = a.from_slice(a); + CHECK(c.size() == a.size()); + } + td::clear_thread_locals(); + ASSERT_EQ(start_mem, td::BufferAllocator::get_buffer_mem()); + ASSERT_EQ(start_size, td::BufferAllocator::get_buffer_slice_size()); + for (int i = 0; i < 20; i++) { + td::ChainBufferWriter input_writer; auto input = input_writer.extract_reader(); - HttpReader reader; + td::HttpReader reader; int max_post_size = 10000; reader.init(&input, max_post_size, 0); - std::srand(4); - std::vector contents(1000); + td::vector contents(100); std::generate(contents.begin(), contents.end(), gen_http_content); auto v = td::transform(contents, rand_http_query); - auto vec_str = rand_split(join(v)); + auto vec_str = td::rand_split(join(v)); - HttpQuery q; - std::vector res; + td::HttpQuery q; + td::vector res; for (auto &str : vec_str) { input_writer.append(str); input.sync_with_writer(); while (true) { auto r_state = reader.read_next(&q); - LOG_IF(ERROR, r_state.is_error()) << r_state.error() << tag("ok", res.size()); + LOG_IF(ERROR, r_state.is_error()) << r_state.error() << td::tag("ok", res.size()); ASSERT_TRUE(r_state.is_ok()); auto state = r_state.ok(); if (state == 0) { @@ -161,11 +186,11 @@ TEST(Http, reader) { ASSERT_EQ(expected, q.content_.str()); res.push_back(q.content_.str()); } else { - auto r_fd = FileFd::open(q.files_[0].temp_file_name, FileFd::Read); + auto r_fd = td::FileFd::open(q.files_[0].temp_file_name, td::FileFd::Read); ASSERT_TRUE(r_fd.is_ok()); auto fd = r_fd.move_as_ok(); - string content(td::narrow_cast(q.files_[0].size), '\0'); - auto r_size = fd.read(MutableSlice(content)); + td::string content(td::narrow_cast(q.files_[0].size), '\0'); + auto r_size = fd.read(td::MutableSlice(content)); ASSERT_TRUE(r_size.is_ok()); ASSERT_TRUE(r_size.ok() == content.size()); ASSERT_TRUE(td::narrow_cast(content.size()) > max_post_size); @@ -181,23 +206,26 @@ TEST(Http, reader) { ASSERT_EQ(contents.size(), res.size()); ASSERT_EQ(contents, res); } - clear_thread_locals(); - ASSERT_EQ(start_mem, BufferAllocator::get_buffer_mem()); + td::clear_thread_locals(); + ASSERT_EQ(start_mem, td::BufferAllocator::get_buffer_mem()); + ASSERT_EQ(start_size, td::BufferAllocator::get_buffer_slice_size()); } TEST(Http, gzip_bomb) { -#if TD_ANDROID || TD_TIZEN || TD_EMSCRIPTEN // the test should be disabled on low-memory systems +#if TD_ANDROID || TD_TIZEN || TD_EMSCRIPTEN // the test must be disabled on low-memory systems return; #endif auto gzip_bomb_str = - gzdecode(gzdecode(base64url_decode(Slice(gzip_bomb, gzip_bomb_size)).ok()).as_slice()).as_slice().str(); + td::gzdecode(td::gzdecode(td::base64url_decode(td::Slice(gzip_bomb, gzip_bomb_size)).ok()).as_slice()) + .as_slice() + .str(); - auto query = make_http_query("", false, true, 0.01, gzip_bomb_str); - auto parts = rand_split(query); - auto input_writer = ChainBufferWriter::create_empty(); + auto query = make_http_query("", false, false, true, 0.01, gzip_bomb_str); + auto parts = td::rand_split(query); + td::ChainBufferWriter input_writer; auto input = input_writer.extract_reader(); - HttpReader reader; - HttpQuery q; + td::HttpReader reader; + td::HttpQuery q; reader.init(&input, 100000000, 0); for (auto &part : parts) { input_writer.append(part); @@ -211,21 +239,40 @@ TEST(Http, gzip_bomb) { } } +TEST(Http, gzip) { + auto gzip_str = td::gzdecode(td::base64url_decode(td::Slice(gzip, gzip_size)).ok()).as_slice().str(); + + td::ChainBufferWriter input_writer; + auto input = input_writer.extract_reader(); + + td::HttpReader reader; + reader.init(&input, 0, 0); + + auto query = make_http_query("", true, false, true, 0.01, gzip_str); + input_writer.append(query); + input.sync_with_writer(); + + td::HttpQuery q; + auto r_state = reader.read_next(&q); + ASSERT_TRUE(r_state.is_error()); + ASSERT_EQ(413, r_state.error().code()); +} + TEST(Http, aes_ctr_encode_decode_flow) { - auto str = rand_string('a', 'z', 1000000); - auto parts = rand_split(str); - auto input_writer = ChainBufferWriter::create_empty(); + auto str = td::rand_string('a', 'z', 1000000); + auto parts = td::rand_split(str); + td::ChainBufferWriter input_writer; auto input = input_writer.extract_reader(); - ByteFlowSource source(&input); - UInt256 key; - UInt128 iv; - Random::secure_bytes(key.raw, sizeof(key)); - Random::secure_bytes(iv.raw, sizeof(iv)); - AesCtrByteFlow aes_encode; + td::ByteFlowSource source(&input); + td::UInt256 key; + td::UInt128 iv; + td::Random::secure_bytes(key.raw, sizeof(key)); + td::Random::secure_bytes(iv.raw, sizeof(iv)); + td::AesCtrByteFlow aes_encode; aes_encode.init(key, iv); - AesCtrByteFlow aes_decode; + td::AesCtrByteFlow aes_decode; aes_decode.init(key, iv); - ByteFlowSink sink; + td::ByteFlowSink sink; source >> aes_encode >> aes_decode >> sink; ASSERT_TRUE(!sink.is_ready()); @@ -234,7 +281,7 @@ TEST(Http, aes_ctr_encode_decode_flow) { source.wakeup(); } ASSERT_TRUE(!sink.is_ready()); - source.close_input(Status::OK()); + source.close_input(td::Status::OK()); ASSERT_TRUE(sink.is_ready()); LOG_IF(ERROR, sink.status().is_error()) << sink.status(); ASSERT_TRUE(sink.status().is_ok()); @@ -242,25 +289,25 @@ TEST(Http, aes_ctr_encode_decode_flow) { } TEST(Http, aes_file_encryption) { - auto str = rand_string('a', 'z', 1000000); - CSlice name = "test_encryption"; - unlink(name).ignore(); - UInt256 key; - UInt128 iv; - Random::secure_bytes(key.raw, sizeof(key)); - Random::secure_bytes(iv.raw, sizeof(iv)); + auto str = td::rand_string('a', 'z', 1000000); + td::CSlice name = "test_encryption"; + td::unlink(name).ignore(); + td::UInt256 key; + td::UInt128 iv; + td::Random::secure_bytes(key.raw, sizeof(key)); + td::Random::secure_bytes(iv.raw, sizeof(iv)); { - BufferedFdBase fd(FileFd::open(name, FileFd::Write | FileFd::Create).move_as_ok()); + td::BufferedFdBase fd(td::FileFd::open(name, td::FileFd::Write | td::FileFd::Create).move_as_ok()); - auto parts = rand_split(str); + auto parts = td::rand_split(str); - ChainBufferWriter output_writer; + td::ChainBufferWriter output_writer; auto output_reader = output_writer.extract_reader(); - ByteFlowSource source(&output_reader); - AesCtrByteFlow aes_encode; + td::ByteFlowSource source(&output_reader); + td::AesCtrByteFlow aes_encode; aes_encode.init(key, iv); - ByteFlowSink sink; + td::ByteFlowSink sink; source >> aes_encode >> sink; fd.set_output_reader(sink.get_output()); @@ -274,26 +321,26 @@ TEST(Http, aes_file_encryption) { } { - BufferedFdBase fd(FileFd::open(name, FileFd::Read).move_as_ok()); + td::BufferedFdBase fd(td::FileFd::open(name, td::FileFd::Read).move_as_ok()); - ChainBufferWriter input_writer; + td::ChainBufferWriter input_writer; auto input_reader = input_writer.extract_reader(); - ByteFlowSource source(&input_reader); - AesCtrByteFlow aes_encode; + td::ByteFlowSource source(&input_reader); + td::AesCtrByteFlow aes_encode; aes_encode.init(key, iv); - ByteFlowSink sink; + td::ByteFlowSink sink; source >> aes_encode >> sink; fd.set_input_writer(&input_writer); - fd.update_flags(Fd::Flag::Read); - while (can_read(fd)) { + fd.get_poll_info().add_flags(td::PollFlags::Read()); + while (can_read_local(fd)) { fd.flush_read(4096).ensure(); source.wakeup(); } fd.close(); - source.close_input(Status::OK()); + source.close_input(td::Status::OK()); ASSERT_TRUE(sink.is_ready()); LOG_IF(ERROR, sink.status().is_error()) << sink.status(); ASSERT_TRUE(sink.status().is_ok()); @@ -303,20 +350,20 @@ TEST(Http, aes_file_encryption) { } TEST(Http, chunked_flow) { - auto str = rand_string('a', 'z', 100); - auto parts = rand_split(make_chunked(str)); - auto input_writer = ChainBufferWriter::create_empty(); + auto str = td::rand_string('a', 'z', 100); + auto parts = td::rand_split(make_chunked(str)); + td::ChainBufferWriter input_writer; auto input = input_writer.extract_reader(); - ByteFlowSource source(&input); - HttpChunkedByteFlow chunked_flow; - ByteFlowSink sink; + td::ByteFlowSource source(&input); + td::HttpChunkedByteFlow chunked_flow; + td::ByteFlowSink sink; source >> chunked_flow >> sink; for (auto &part : parts) { input_writer.append(part); source.wakeup(); } - source.close_input(Status::OK()); + source.close_input(td::Status::OK()); ASSERT_TRUE(sink.is_ready()); LOG_IF(ERROR, sink.status().is_error()) << sink.status(); ASSERT_TRUE(sink.status().is_ok()); @@ -326,16 +373,16 @@ TEST(Http, chunked_flow) { } TEST(Http, chunked_flow_error) { - auto str = rand_string('a', 'z', 100000); + auto str = td::rand_string('a', 'z', 100000); for (int d = 1; d < 100; d += 10) { auto new_str = make_chunked(str); new_str.resize(str.size() - d); - auto parts = rand_split(new_str); - auto input_writer = ChainBufferWriter::create_empty(); + auto parts = td::rand_split(new_str); + td::ChainBufferWriter input_writer; auto input = input_writer.extract_reader(); - ByteFlowSource source(&input); - HttpChunkedByteFlow chunked_flow; - ByteFlowSink sink; + td::ByteFlowSource source(&input); + td::HttpChunkedByteFlow chunked_flow; + td::ByteFlowSink sink; source >> chunked_flow >> sink; for (auto &part : parts) { @@ -343,31 +390,106 @@ TEST(Http, chunked_flow_error) { source.wakeup(); } ASSERT_TRUE(!sink.is_ready()); - source.close_input(Status::OK()); + source.close_input(td::Status::OK()); ASSERT_TRUE(sink.is_ready()); ASSERT_TRUE(!sink.status().is_ok()); } } TEST(Http, gzip_chunked_flow) { - auto str = rand_string('a', 'z', 1000000); - auto parts = rand_split(make_chunked(gzencode(str).as_slice().str())); + auto str = td::rand_string('a', 'z', 1000000); + auto parts = td::rand_split(make_chunked(td::gzencode(str, 2.0).as_slice().str())); - auto input_writer = ChainBufferWriter::create_empty(); + td::ChainBufferWriter input_writer; auto input = input_writer.extract_reader(); - ByteFlowSource source(&input); - HttpChunkedByteFlow chunked_flow; - GzipByteFlow gzip_flow(Gzip::Decode); - ByteFlowSink sink; + td::ByteFlowSource source(&input); + td::HttpChunkedByteFlow chunked_flow; + td::GzipByteFlow gzip_flow(td::Gzip::Mode::Decode); + td::ByteFlowSink sink; source >> chunked_flow >> gzip_flow >> sink; for (auto &part : parts) { input_writer.append(part); source.wakeup(); } - source.close_input(Status::OK()); + source.close_input(td::Status::OK()); ASSERT_TRUE(sink.is_ready()); LOG_IF(ERROR, sink.status().is_error()) << sink.status(); ASSERT_TRUE(sink.status().is_ok()); ASSERT_EQ(str, sink.result()->move_as_buffer_slice().as_slice().str()); } + +TEST(Http, gzip_bomb_with_limit) { + td::string gzip_bomb_str; + { + td::ChainBufferWriter input_writer; + auto input = input_writer.extract_reader(); + td::GzipByteFlow gzip_flow(td::Gzip::Mode::Encode); + td::ByteFlowSource source(&input); + td::ByteFlowSink sink; + source >> gzip_flow >> sink; + + td::string s(1 << 16, 'a'); + for (int i = 0; i < 1000; i++) { + input_writer.append(s); + source.wakeup(); + } + source.close_input(td::Status::OK()); + ASSERT_TRUE(sink.is_ready()); + LOG_IF(ERROR, sink.status().is_error()) << sink.status(); + ASSERT_TRUE(sink.status().is_ok()); + gzip_bomb_str = sink.result()->move_as_buffer_slice().as_slice().str(); + } + + auto query = make_http_query("", false, false, true, 0.01, gzip_bomb_str); + auto parts = td::rand_split(query); + td::ChainBufferWriter input_writer; + auto input = input_writer.extract_reader(); + td::HttpReader reader; + td::HttpQuery q; + reader.init(&input, 1000000); + bool ok = false; + for (auto &part : parts) { + input_writer.append(part); + input.sync_with_writer(); + auto r_state = reader.read_next(&q); + if (r_state.is_error()) { + LOG(FATAL) << r_state.error(); + return; + } else if (r_state.ok() == 0) { + ok = true; + } + } + ASSERT_TRUE(ok); +} + +#if TD_DARWIN_WATCH_OS +struct Baton { + std::mutex mutex; + std::condition_variable cond; + bool is_ready{false}; + + void wait() { + std::unique_lock lock(mutex); + cond.wait(lock, [&] { return is_ready; }); + } + + void post() { + { + std::unique_lock lock(mutex); + is_ready = true; + } + cond.notify_all(); + } + + void reset() { + is_ready = false; + } +}; + +TEST(Http, Darwin) { + Baton baton; + td::DarwinHttp::get("http://example.com", [&](td::BufferSlice data) { baton.post(); }); + baton.wait(); +} +#endif diff --git a/protocols/Telegram/tdlib/td/test/link.cpp b/protocols/Telegram/tdlib/td/test/link.cpp new file mode 100644 index 0000000000..13c4613174 --- /dev/null +++ b/protocols/Telegram/tdlib/td/test/link.cpp @@ -0,0 +1,983 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#include "td/telegram/LinkManager.h" + +#include "td/telegram/MessageEntity.h" +#include "td/telegram/td_api.h" + +#include "td/utils/common.h" +#include "td/utils/logging.h" +#include "td/utils/misc.h" +#include "td/utils/tests.h" + +static void check_find_urls(const td::string &url, bool is_valid) { + auto url_lower = td::to_lower(url); + { + auto tg_urls = td::find_tg_urls(url); + if (is_valid && (td::begins_with(url_lower, "tg://") || td::begins_with(url_lower, "ton://"))) { + ASSERT_EQ(1u, tg_urls.size()); + ASSERT_STREQ(url, tg_urls[0]); + } else { + ASSERT_TRUE(tg_urls.empty() || tg_urls[0] != url); + } + } + + { + if (is_valid && (td::begins_with(url_lower, "http") || td::begins_with(url_lower, "t.me")) && + url.find('.') != td::string::npos && url.find(' ') == td::string::npos && url != "http://.." && + url.find("ra.ph") == td::string::npos && url.find("Aph") == td::string::npos) { + auto urls = td::find_urls(url); + ASSERT_EQ(1u, urls.size()); + ASSERT_STREQ(url, urls[0].first); + } + } +} + +static void check_link(const td::string &url, const td::string &expected) { + auto result = td::LinkManager::check_link(url); + if (result.is_ok()) { + ASSERT_STREQ(expected, result.ok()); + } else { + ASSERT_TRUE(expected.empty()); + } + + check_find_urls(url, result.is_ok()); +} + +TEST(Link, check_link) { + check_link("sftp://google.com", ""); + check_link("tg://google_com", "tg://google_com/"); + check_link("tOn://google", "ton://google/"); + check_link("httP://google.com?1#tes", "http://google.com/?1#tes"); + check_link("httPs://google.com/?1#tes", "https://google.com/?1#tes"); + check_link("http://google.com:0", ""); + check_link("http://google.com:0000000001", "http://google.com:1/"); + check_link("http://google.com:-1", ""); + check_link("tg://google?1#tes", "tg://google?1#tes"); + check_link("tg://google/?1#tes", "tg://google?1#tes"); + check_link("TG:_", "tg://_/"); + check_link("sftp://google.com", ""); + check_link("sftp://google.com", ""); + check_link("http:google.com", ""); + check_link("tg://http://google.com", ""); + check_link("tg:http://google.com", ""); + check_link("tg:https://google.com", ""); + check_link("tg:test@google.com", ""); + check_link("tg:google.com:80", ""); + check_link("tg:google-com", "tg://google-com/"); + check_link("tg:google.com", ""); + check_link("tg:google.com:0", ""); + check_link("tg:google.com:a", ""); + check_link("tg:[2001:db8:0:0:0:ff00:42:8329]", ""); + check_link("tg:127.0.0.1", ""); + check_link("http://[2001:db8:0:0:0:ff00:42:8329]", "http://[2001:db8:0:0:0:ff00:42:8329]/"); + check_link("http://localhost", ""); + check_link("http://..", "http://../"); + check_link("..", "http://../"); + check_link("https://.", ""); +} + +static void parse_internal_link(const td::string &url, td::td_api::object_ptr expected) { + auto result = td::LinkManager::parse_internal_link(url); + if (result != nullptr) { + auto object = result->get_internal_link_type_object(); + if (object->get_id() == td::td_api::internalLinkTypeMessageDraft::ID) { + static_cast(object.get())->text_->entities_.clear(); + } + ASSERT_STREQ(url + ' ' + to_string(expected), url + ' ' + to_string(object)); + } else { + LOG_IF(ERROR, expected != nullptr) << url; + ASSERT_TRUE(expected == nullptr); + } + + check_find_urls(url, result != nullptr); +} + +TEST(Link, parse_internal_link) { + auto chat_administrator_rights = [](bool can_manage_chat, bool can_change_info, bool can_post_messages, + bool can_edit_messages, bool can_delete_messages, bool can_invite_users, + bool can_restrict_members, bool can_pin_messages, bool can_manage_topics, + bool can_promote_members, bool can_manage_video_chats, bool is_anonymous) { + return td::td_api::make_object( + can_manage_chat, can_change_info, can_post_messages, can_edit_messages, can_delete_messages, can_invite_users, + can_restrict_members, can_pin_messages, can_manage_topics, can_promote_members, can_manage_video_chats, + is_anonymous); + }; + auto target_chat_chosen = [](bool allow_users, bool allow_bots, bool allow_groups, bool allow_channels) { + return td::td_api::make_object(allow_users, allow_bots, allow_groups, allow_channels); + }; + + auto active_sessions = [] { + return td::td_api::make_object(); + }; + auto attachment_menu_bot = [](td::td_api::object_ptr chat_types, + td::td_api::object_ptr chat_link, + const td::string &bot_username, const td::string &start_parameter) { + td::td_api::object_ptr target_chat; + if (chat_link != nullptr) { + target_chat = td::td_api::make_object(std::move(chat_link)); + } else if (chat_types != nullptr) { + target_chat = std::move(chat_types); + } else { + target_chat = td::td_api::make_object(); + } + return td::td_api::make_object( + std::move(target_chat), bot_username, start_parameter.empty() ? td::string() : "start://" + start_parameter); + }; + auto authentication_code = [](const td::string &code) { + return td::td_api::make_object(code); + }; + auto background = [](const td::string &background_name) { + return td::td_api::make_object(background_name); + }; + auto bot_add_to_channel = [](const td::string &bot_username, + td::td_api::object_ptr &&administrator_rights) { + return td::td_api::make_object(bot_username, + std::move(administrator_rights)); + }; + auto bot_start = [](const td::string &bot_username, const td::string &start_parameter) { + return td::td_api::make_object(bot_username, start_parameter, false); + }; + auto bot_start_in_group = [](const td::string &bot_username, const td::string &start_parameter, + td::td_api::object_ptr &&administrator_rights) { + return td::td_api::make_object(bot_username, start_parameter, + std::move(administrator_rights)); + }; + auto change_phone_number = [] { + return td::td_api::make_object(); + }; + auto chat_invite = [](const td::string &hash) { + return td::td_api::make_object("tg:join?invite=" + hash); + }; + auto filter_settings = [] { + return td::td_api::make_object(); + }; + auto game = [](const td::string &bot_username, const td::string &game_short_name) { + return td::td_api::make_object(bot_username, game_short_name); + }; + auto instant_view = [](const td::string &url, const td::string &fallback_url) { + return td::td_api::make_object(url, fallback_url); + }; + auto invoice = [](const td::string &invoice_name) { + return td::td_api::make_object(invoice_name); + }; + auto language_pack = [](const td::string &language_pack_name) { + return td::td_api::make_object(language_pack_name); + }; + auto language_settings = [] { + return td::td_api::make_object(); + }; + auto message = [](const td::string &url) { + return td::td_api::make_object(url); + }; + auto message_draft = [](td::string text, bool contains_url) { + auto formatted_text = td::td_api::make_object(); + formatted_text->text_ = std::move(text); + return td::td_api::make_object(std::move(formatted_text), contains_url); + }; + auto passport_data_request = [](td::int32 bot_user_id, const td::string &scope, const td::string &public_key, + const td::string &nonce, const td::string &callback_url) { + return td::td_api::make_object(bot_user_id, scope, public_key, + nonce, callback_url); + }; + auto phone_number_confirmation = [](const td::string &hash, const td::string &phone_number) { + return td::td_api::make_object(hash, phone_number); + }; + auto premium_features = [](const td::string &referrer) { + return td::td_api::make_object(referrer); + }; + auto privacy_and_security_settings = [] { + return td::td_api::make_object(); + }; + auto proxy_mtproto = [](const td::string &server, td::int32 port, const td::string &secret) { + return td::td_api::make_object( + server, port, td::td_api::make_object(secret)); + }; + auto proxy_socks = [](const td::string &server, td::int32 port, const td::string &username, + const td::string &password) { + return td::td_api::make_object( + server, port, td::td_api::make_object(username, password)); + }; + auto public_chat = [](const td::string &chat_username) { + return td::td_api::make_object(chat_username); + }; + auto qr_code_authentication = [] { + return td::td_api::make_object(); + }; + auto restore_purchases = [] { + return td::td_api::make_object(); + }; + auto settings = [] { + return td::td_api::make_object(); + }; + auto sticker_set = [](const td::string &sticker_set_name) { + return td::td_api::make_object(sticker_set_name); + }; + auto theme = [](const td::string &theme_name) { + return td::td_api::make_object(theme_name); + }; + auto theme_settings = [] { + return td::td_api::make_object(); + }; + auto unknown_deep_link = [](const td::string &link) { + return td::td_api::make_object(link); + }; + auto unsupported_proxy = [] { + return td::td_api::make_object(); + }; + auto user_phone_number = [](const td::string &phone_number) { + return td::td_api::make_object(phone_number); + }; + auto video_chat = [](const td::string &chat_username, const td::string &invite_hash, bool is_live_stream) { + return td::td_api::make_object(chat_username, invite_hash, is_live_stream); + }; + + parse_internal_link("t.me/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("telegram.me/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("telegram.dog/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("www.t.me/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("www%2etelegram.me/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("www%2Etelegram.dog/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("www%252Etelegram.dog/levlam/1", nullptr); + parse_internal_link("www.t.me/s/s/s/s/s/joinchat/1", chat_invite("1")); + parse_internal_link("www.t.me/s/%73/%73/s/%73/joinchat/1", chat_invite("1")); + parse_internal_link("http://t.me/s/s/s/s/s/s/s/s/s/s/s/s/s/s/s/s/s/joinchat/1", chat_invite("1")); + parse_internal_link("http://t.me/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("https://t.me/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("hTtp://www.t.me:443/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("httPs://t.me:80/levlam/1", message("tg:resolve?domain=levlam&post=1")); + parse_internal_link("https://t.me:200/levlam/1", nullptr); + parse_internal_link("http:t.me/levlam/1", nullptr); + parse_internal_link("t.dog/levlam/1", nullptr); + parse_internal_link("t.m/levlam/1", nullptr); + parse_internal_link("t.men/levlam/1", nullptr); + + parse_internal_link("tg:resolve?domain=username&post=12345&single", + message("tg:resolve?domain=username&post=12345&single")); + parse_internal_link("tg:resolve?domain=username&post=12345&single&startattach=1&attach=test", + message("tg:resolve?domain=username&post=12345&single")); + parse_internal_link("tg:resolve?domain=user%31name&post=%312345&single&comment=456&t=789&single&thread=123%20%31", + message("tg:resolve?domain=user1name&post=12345&single&thread=123%201&comment=456&t=789")); + parse_internal_link("TG://resolve?domain=username&post=12345&single&voicechat=aasd", + message("tg:resolve?domain=username&post=12345&single")); + parse_internal_link("TG://test@resolve?domain=username&post=12345&single", nullptr); + parse_internal_link("tg:resolve:80?domain=username&post=12345&single", nullptr); + parse_internal_link("tg:http://resolve?domain=username&post=12345&single", nullptr); + parse_internal_link("tg:https://resolve?domain=username&post=12345&single", nullptr); + parse_internal_link("tg:resolve?domain=&post=12345&single", + unknown_deep_link("tg://resolve?domain=&post=12345&single")); + parse_internal_link("tg:resolve?domain=telegram&post=&single", public_chat("telegram")); + parse_internal_link("tg:resolve?domain=123456&post=&single", + unknown_deep_link("tg://resolve?domain=123456&post=&single")); + parse_internal_link("tg:resolve?domain=telegram&startattach", attachment_menu_bot(nullptr, nullptr, "telegram", "")); + parse_internal_link("tg:resolve?domain=telegram&startattach=1", + attachment_menu_bot(nullptr, nullptr, "telegram", "1")); + parse_internal_link("tg:resolve?domain=telegram&startattach=1&choose=cats+dogs", + attachment_menu_bot(nullptr, nullptr, "telegram", "1")); + parse_internal_link("tg:resolve?domain=telegram&startattach=1&choose=users", + attachment_menu_bot(target_chat_chosen(true, false, false, false), nullptr, "telegram", "1")); + parse_internal_link("tg:resolve?domain=telegram&startattach=1&choose=bots", + attachment_menu_bot(target_chat_chosen(false, true, false, false), nullptr, "telegram", "1")); + parse_internal_link("tg:resolve?domain=telegram&startattach=1&choose=groups", + attachment_menu_bot(target_chat_chosen(false, false, true, false), nullptr, "telegram", "1")); + parse_internal_link("tg:resolve?domain=telegram&startattach=1&choose=channels", + attachment_menu_bot(target_chat_chosen(false, false, false, true), nullptr, "telegram", "1")); + parse_internal_link("tg:resolve?domain=telegram&startattach=1&choose=users+channels", + attachment_menu_bot(target_chat_chosen(true, false, false, true), nullptr, "telegram", "1")); + parse_internal_link("tg:resolve?domain=telegram&attach=&startattach", + attachment_menu_bot(nullptr, nullptr, "telegram", "")); + parse_internal_link("tg:resolve?domain=telegram&attach=&startattach=1", + attachment_menu_bot(nullptr, nullptr, "telegram", "1")); + parse_internal_link("tg:resolve?domain=telegram&attach=test&startattach", + attachment_menu_bot(nullptr, public_chat("telegram"), "test", "")); + parse_internal_link("tg:resolve?domain=telegram&attach=test&startattach=1", + attachment_menu_bot(nullptr, public_chat("telegram"), "test", "1")); + + parse_internal_link("tg:resolve?phone=1", user_phone_number("1")); + parse_internal_link("tg:resolve?phone=123456", user_phone_number("123456")); + parse_internal_link("tg:resolve?phone=123456&startattach", user_phone_number("123456")); + parse_internal_link("tg:resolve?phone=123456&startattach=123", user_phone_number("123456")); + parse_internal_link("tg:resolve?phone=123456&attach=", user_phone_number("123456")); + parse_internal_link("tg:resolve?phone=123456&attach=&startattach", user_phone_number("123456")); + parse_internal_link("tg:resolve?phone=123456&attach=&startattach=123", user_phone_number("123456")); + parse_internal_link("tg:resolve?phone=123456&attach=test", + attachment_menu_bot(nullptr, user_phone_number("123456"), "test", "")); + parse_internal_link("tg:resolve?phone=123456&attach=test&startattach&choose=users", + attachment_menu_bot(nullptr, user_phone_number("123456"), "test", "")); + parse_internal_link("tg:resolve?phone=123456&attach=test&startattach=123", + attachment_menu_bot(nullptr, user_phone_number("123456"), "test", "123")); + parse_internal_link("tg:resolve?phone=01234567890123456789012345678912", + user_phone_number("01234567890123456789012345678912")); + parse_internal_link("tg:resolve?phone=012345678901234567890123456789123", + unknown_deep_link("tg://resolve?phone=012345678901234567890123456789123")); + parse_internal_link("tg:resolve?phone=", unknown_deep_link("tg://resolve?phone=")); + parse_internal_link("tg:resolve?phone=+123", unknown_deep_link("tg://resolve?phone=+123")); + parse_internal_link("tg:resolve?phone=123456 ", unknown_deep_link("tg://resolve?phone=123456 ")); + + parse_internal_link("t.me/username/12345?single", message("tg:resolve?domain=username&post=12345&single")); + parse_internal_link("t.me/username/12345?asdasd", message("tg:resolve?domain=username&post=12345")); + parse_internal_link("t.me/username/12345", message("tg:resolve?domain=username&post=12345")); + parse_internal_link("t.me/username/12345/", message("tg:resolve?domain=username&post=12345")); + parse_internal_link("t.me/username/12345#asdasd", message("tg:resolve?domain=username&post=12345")); + parse_internal_link("t.me/username/12345//?voicechat=&single", + message("tg:resolve?domain=username&post=12345&single")); + parse_internal_link("t.me/username/12345/asdasd//asd/asd/asd/?single", + message("tg:resolve?domain=username&post=12345&single")); + parse_internal_link("t.me/username/12345/67890/asdasd//asd/asd/asd/?single", + message("tg:resolve?domain=username&post=67890&single&thread=12345")); + parse_internal_link("t.me/username/1asdasdas/asdasd//asd/asd/asd/?single", + message("tg:resolve?domain=username&post=1&single")); + parse_internal_link("t.me/username/asd", public_chat("username")); + parse_internal_link("t.me/username/0", public_chat("username")); + parse_internal_link("t.me/username/-12345", public_chat("username")); + parse_internal_link("t.me//12345?single", nullptr); + parse_internal_link("https://telegram.dog/telegram/?single", public_chat("telegram")); + parse_internal_link("t.me/username?startattach", attachment_menu_bot(nullptr, nullptr, "username", "")); + parse_internal_link("t.me/username?startattach=1", attachment_menu_bot(nullptr, nullptr, "username", "1")); + parse_internal_link("t.me/username?startattach=1&choose=cats+dogs", + attachment_menu_bot(nullptr, nullptr, "username", "1")); + parse_internal_link("t.me/username?startattach=1&choose=users", + attachment_menu_bot(target_chat_chosen(true, false, false, false), nullptr, "username", "1")); + parse_internal_link("t.me/username?startattach=1&choose=bots", + attachment_menu_bot(target_chat_chosen(false, true, false, false), nullptr, "username", "1")); + parse_internal_link("t.me/username?startattach=1&choose=groups", + attachment_menu_bot(target_chat_chosen(false, false, true, false), nullptr, "username", "1")); + parse_internal_link("t.me/username?startattach=1&choose=channels", + attachment_menu_bot(target_chat_chosen(false, false, false, true), nullptr, "username", "1")); + parse_internal_link("t.me/username?startattach=1&choose=bots+groups", + attachment_menu_bot(target_chat_chosen(false, true, true, false), nullptr, "username", "1")); + parse_internal_link("t.me/username?attach=", public_chat("username")); + parse_internal_link("t.me/username?attach=&startattach", attachment_menu_bot(nullptr, nullptr, "username", "")); + parse_internal_link("t.me/username?attach=&startattach=1", attachment_menu_bot(nullptr, nullptr, "username", "1")); + parse_internal_link("t.me/username?attach=bot", attachment_menu_bot(nullptr, public_chat("username"), "bot", "")); + parse_internal_link("t.me/username?attach=bot&startattach", + attachment_menu_bot(nullptr, public_chat("username"), "bot", "")); + parse_internal_link("t.me/username?attach=bot&startattach=1&choose=users", + attachment_menu_bot(nullptr, public_chat("username"), "bot", "1")); + + parse_internal_link("tg:privatepost?domain=username/12345&single", + unknown_deep_link("tg://privatepost?domain=username/12345&single")); + parse_internal_link("tg:privatepost?channel=username/12345&single", + unknown_deep_link("tg://privatepost?channel=username/12345&single")); + parse_internal_link("tg:privatepost?channel=username&post=12345", + message("tg:privatepost?channel=username&post=12345")); + + parse_internal_link("t.me/c/12345?single", nullptr); + parse_internal_link("t.me/c/1/c?single", nullptr); + parse_internal_link("t.me/c/c/1?single", nullptr); + parse_internal_link("t.me/c//1?single", nullptr); + parse_internal_link("t.me/c/12345/123", message("tg:privatepost?channel=12345&post=123")); + parse_internal_link("t.me/c/12345/123?single", message("tg:privatepost?channel=12345&post=123&single")); + parse_internal_link("t.me/c/12345/123/asd/asd////?single", message("tg:privatepost?channel=12345&post=123&single")); + parse_internal_link("t.me/c/12345/123/456/asd/asd////?single", + message("tg:privatepost?channel=12345&post=456&single&thread=123")); + parse_internal_link("t.me/c/%312345/%3123?comment=456&t=789&single&thread=123%20%31", + message("tg:privatepost?channel=12345&post=123&single&thread=123%201&comment=456&t=789")); + + parse_internal_link("tg:bg?color=111111#asdasd", background("111111")); + parse_internal_link("tg:bg?color=11111%31", background("111111")); + parse_internal_link("tg:bg?color=11111%20", background("11111%20")); + parse_internal_link("tg:bg?gradient=111111-222222", background("111111-222222")); + parse_internal_link("tg:bg?rotation=180%20&gradient=111111-222222%20", + background("111111-222222%20?rotation=180%20")); + parse_internal_link("tg:bg?gradient=111111~222222", background("111111~222222")); + parse_internal_link("tg:bg?gradient=abacaba", background("abacaba")); + parse_internal_link("tg:bg?slug=111111~222222#asdasd", background("111111~222222")); + parse_internal_link("tg:bg?slug=111111~222222&mode=12", background("111111~222222?mode=12")); + parse_internal_link("tg:bg?slug=111111~222222&mode=12&text=1", background("111111~222222?mode=12")); + parse_internal_link("tg:bg?slug=111111~222222&mode=12&mode=1", background("111111~222222?mode=12")); + parse_internal_link("tg:bg?slug=test&mode=12&rotation=4&intensity=2&bg_color=3", + background("test?mode=12&intensity=2&bg_color=3&rotation=4")); + parse_internal_link("tg:bg?mode=12&&slug=test&intensity=2&bg_color=3", + background("test?mode=12&intensity=2&bg_color=3")); + parse_internal_link("tg:bg?mode=12&intensity=2&bg_color=3", + unknown_deep_link("tg://bg?mode=12&intensity=2&bg_color=3")); + + parse_internal_link("tg:bg?color=111111#asdasd", background("111111")); + parse_internal_link("tg:bg?color=11111%31", background("111111")); + parse_internal_link("tg:bg?color=11111%20", background("11111%20")); + parse_internal_link("tg:bg?gradient=111111-222222", background("111111-222222")); + parse_internal_link("tg:bg?rotation=180%20&gradient=111111-222222%20", + background("111111-222222%20?rotation=180%20")); + parse_internal_link("tg:bg?gradient=111111~222222", background("111111~222222")); + parse_internal_link("tg:bg?gradient=abacaba", background("abacaba")); + parse_internal_link("tg:bg?slug=111111~222222#asdasd", background("111111~222222")); + parse_internal_link("tg:bg?slug=111111~222222&mode=12", background("111111~222222?mode=12")); + parse_internal_link("tg:bg?slug=111111~222222&mode=12&text=1", background("111111~222222?mode=12")); + parse_internal_link("tg:bg?slug=111111~222222&mode=12&mode=1", background("111111~222222?mode=12")); + parse_internal_link("tg:bg?slug=test&mode=12&rotation=4&intensity=2&bg_color=3", + background("test?mode=12&intensity=2&bg_color=3&rotation=4")); + parse_internal_link("tg:bg?mode=12&&slug=test&intensity=2&bg_color=3", + background("test?mode=12&intensity=2&bg_color=3")); + parse_internal_link("tg:bg?mode=12&intensity=2&bg_color=3", + unknown_deep_link("tg://bg?mode=12&intensity=2&bg_color=3")); + + parse_internal_link("%54.me/bg/111111#asdasd", background("111111")); + parse_internal_link("t.me/bg/11111%31", background("111111")); + parse_internal_link("t.me/bg/11111%20", background("11111%20")); + parse_internal_link("t.me/bg/111111-222222", background("111111-222222")); + parse_internal_link("t.me/bg/111111-222222%20?rotation=180%20", background("111111-222222%20?rotation=180%20")); + parse_internal_link("t.me/bg/111111~222222", background("111111~222222")); + parse_internal_link("t.me/bg/abacaba", background("abacaba")); + parse_internal_link("t.me/Bg/abacaba", public_chat("Bg")); + parse_internal_link("t.me/bg/111111~222222#asdasd", background("111111~222222")); + parse_internal_link("t.me/bg/111111~222222?mode=12", background("111111~222222?mode=12")); + parse_internal_link("t.me/bg/111111~222222?mode=12&text=1", background("111111~222222?mode=12")); + parse_internal_link("t.me/bg/111111~222222?mode=12&mode=1", background("111111~222222?mode=12")); + parse_internal_link("t.me/bg/test?mode=12&rotation=4&intensity=2&bg_color=3", + background("test?mode=12&intensity=2&bg_color=3&rotation=4")); + parse_internal_link("t.me/%62g/test/?mode=12&&&intensity=2&bg_color=3", + background("test?mode=12&intensity=2&bg_color=3")); + parse_internal_link("t.me/bg//", nullptr); + parse_internal_link("t.me/bg/%20/", background("%20")); + parse_internal_link("t.me/bg/", nullptr); + + parse_internal_link("t.me/invoice?slug=abcdef", nullptr); + parse_internal_link("t.me/invoice", nullptr); + parse_internal_link("t.me/invoice/", nullptr); + parse_internal_link("t.me/invoice//abcdef", nullptr); + parse_internal_link("t.me/invoice?/abcdef", nullptr); + parse_internal_link("t.me/invoice/?abcdef", nullptr); + parse_internal_link("t.me/invoice/#abcdef", nullptr); + parse_internal_link("t.me/invoice/abacaba", invoice("abacaba")); + parse_internal_link("t.me/invoice/aba%20aba", invoice("aba aba")); + parse_internal_link("t.me/invoice/123456a", invoice("123456a")); + parse_internal_link("t.me/invoice/12345678901", invoice("12345678901")); + parse_internal_link("t.me/invoice/123456", invoice("123456")); + parse_internal_link("t.me/invoice/123456/123123/12/31/a/s//21w/?asdas#test", invoice("123456")); + + parse_internal_link("t.me/$?slug=abcdef", nullptr); + parse_internal_link("t.me/$", nullptr); + parse_internal_link("t.me/$/abcdef", nullptr); + parse_internal_link("t.me/$?/abcdef", nullptr); + parse_internal_link("t.me/$?abcdef", nullptr); + parse_internal_link("t.me/$#abcdef", nullptr); + parse_internal_link("t.me/$abacaba", invoice("abacaba")); + parse_internal_link("t.me/$aba%20aba", invoice("aba aba")); + parse_internal_link("t.me/$123456a", invoice("123456a")); + parse_internal_link("t.me/$12345678901", invoice("12345678901")); + parse_internal_link("t.me/$123456", invoice("123456")); + parse_internal_link("t.me/%24123456", invoice("123456")); + parse_internal_link("t.me/$123456/123123/12/31/a/s//21w/?asdas#test", invoice("123456")); + + parse_internal_link("tg:invoice?slug=abcdef", invoice("abcdef")); + parse_internal_link("tg:invoice?slug=abc%30ef", invoice("abc0ef")); + parse_internal_link("tg://invoice?slug=", unknown_deep_link("tg://invoice?slug=")); + + parse_internal_link("tg:share?url=google.com&text=text#asdasd", message_draft("google.com\ntext", true)); + parse_internal_link("tg:share?url=google.com&text=", message_draft("google.com", false)); + parse_internal_link("tg:share?url=&text=google.com", message_draft("google.com", false)); + parse_internal_link("tg:msg_url?url=google.com&text=text", message_draft("google.com\ntext", true)); + parse_internal_link("tg:msg_url?url=google.com&text=", message_draft("google.com", false)); + parse_internal_link("tg:msg_url?url=&text=google.com", message_draft("google.com", false)); + parse_internal_link("tg:msg?url=google.com&text=text", message_draft("google.com\ntext", true)); + parse_internal_link("tg:msg?url=google.com&text=", message_draft("google.com", false)); + parse_internal_link("tg:msg?url=&text=google.com", message_draft("google.com", false)); + parse_internal_link("tg:msg?url=&text=\n\n\n\n\n\n\n\n", nullptr); + parse_internal_link("tg:msg?url=%20\n&text=", nullptr); + parse_internal_link("tg:msg?url=%20\n&text=google.com", message_draft("google.com", false)); + parse_internal_link("tg:msg?url=@&text=", message_draft(" @", false)); + parse_internal_link("tg:msg?url=&text=@", message_draft(" @", false)); + parse_internal_link("tg:msg?url=@&text=@", message_draft(" @\n@", true)); + parse_internal_link("tg:msg?url=%FF&text=1", nullptr); + + parse_internal_link("https://t.me/share?url=google.com&text=text#asdasd", message_draft("google.com\ntext", true)); + parse_internal_link("https://t.me/share?url=google.com&text=", message_draft("google.com", false)); + parse_internal_link("https://t.me/share?url=&text=google.com", message_draft("google.com", false)); + parse_internal_link("https://t.me/msg?url=google.com&text=text", message_draft("google.com\ntext", true)); + parse_internal_link("https://t.me/msg?url=google.com&text=", message_draft("google.com", false)); + parse_internal_link("https://t.me/msg?url=&text=google.com", message_draft("google.com", false)); + parse_internal_link("https://t.me/msg?url=google.com&text=text", message_draft("google.com\ntext", true)); + parse_internal_link("https://t.me/msg?url=google.com&text=", message_draft("google.com", false)); + parse_internal_link("https://t.me/msg?url=&text=google.com", message_draft("google.com", false)); + parse_internal_link("https://t.me/msg?url=&text=\n\n\n\n\n\n\n\n", nullptr); + parse_internal_link("https://t.me/msg?url=%20%0A&text=", nullptr); + parse_internal_link("https://t.me/msg?url=%20%0A&text=google.com", message_draft("google.com", false)); + parse_internal_link("https://t.me/msg?url=@&text=", message_draft(" @", false)); + parse_internal_link("https://t.me/msg?url=&text=@", message_draft(" @", false)); + parse_internal_link("https://t.me/msg?url=@&text=@", message_draft(" @\n@", true)); + parse_internal_link("https://t.me/msg?url=%FF&text=1", nullptr); + + parse_internal_link("tg:login?codec=12345", unknown_deep_link("tg://login?codec=12345")); + parse_internal_link("tg:login", unknown_deep_link("tg://login")); + parse_internal_link("tg:login?code=abacaba", authentication_code("abacaba")); + parse_internal_link("tg:login?code=123456", authentication_code("123456")); + + parse_internal_link("t.me/login?codec=12345", nullptr); + parse_internal_link("t.me/login", nullptr); + parse_internal_link("t.me/login/", nullptr); + parse_internal_link("t.me/login//12345", nullptr); + parse_internal_link("t.me/login?/12345", nullptr); + parse_internal_link("t.me/login/?12345", nullptr); + parse_internal_link("t.me/login/#12345", nullptr); + parse_internal_link("t.me/login/abacaba", authentication_code("abacaba")); + parse_internal_link("t.me/login/aba%20aba", authentication_code("aba aba")); + parse_internal_link("t.me/login/123456a", authentication_code("123456a")); + parse_internal_link("t.me/login/12345678901", authentication_code("12345678901")); + parse_internal_link("t.me/login/123456", authentication_code("123456")); + parse_internal_link("t.me/login/123456/123123/12/31/a/s//21w/?asdas#test", authentication_code("123456")); + + parse_internal_link("tg:login?token=abacaba", qr_code_authentication()); + parse_internal_link("tg:login?token=", unknown_deep_link("tg://login?token=")); + + parse_internal_link("tg:restore_purchases?token=abacaba", restore_purchases()); + parse_internal_link("tg:restore_purchases?#", restore_purchases()); + parse_internal_link("tg:restore_purchases/?#", restore_purchases()); + parse_internal_link("tg:restore_purchases", restore_purchases()); + parse_internal_link("tg:restore_purchase", unknown_deep_link("tg://restore_purchase")); + parse_internal_link("tg:restore_purchasess", unknown_deep_link("tg://restore_purchasess")); + parse_internal_link("tg:restore_purchases/test?#", unknown_deep_link("tg://restore_purchases/test?")); + + parse_internal_link("t.me/joinchat?invite=abcdef", nullptr); + parse_internal_link("t.me/joinchat", nullptr); + parse_internal_link("t.me/joinchat/", nullptr); + parse_internal_link("t.me/joinchat//abcdef", nullptr); + parse_internal_link("t.me/joinchat?/abcdef", nullptr); + parse_internal_link("t.me/joinchat/?abcdef", nullptr); + parse_internal_link("t.me/joinchat/#abcdef", nullptr); + parse_internal_link("t.me/joinchat/abacaba", chat_invite("abacaba")); + parse_internal_link("t.me/joinchat/aba%20aba", chat_invite("aba%20aba")); + parse_internal_link("t.me/joinchat/aba%30aba", chat_invite("aba0aba")); + parse_internal_link("t.me/joinchat/123456a", chat_invite("123456a")); + parse_internal_link("t.me/joinchat/12345678901", chat_invite("12345678901")); + parse_internal_link("t.me/joinchat/123456", chat_invite("123456")); + parse_internal_link("t.me/joinchat/123456/123123/12/31/a/s//21w/?asdas#test", chat_invite("123456")); + + parse_internal_link("t.me/+?invite=abcdef", nullptr); + parse_internal_link("t.me/+a", chat_invite("a")); + parse_internal_link("t.me/+", nullptr); + parse_internal_link("t.me/+/abcdef", nullptr); + parse_internal_link("t.me/ ?/abcdef", nullptr); + parse_internal_link("t.me/+?abcdef", nullptr); + parse_internal_link("t.me/+#abcdef", nullptr); + parse_internal_link("t.me/ abacaba", chat_invite("abacaba")); + parse_internal_link("t.me/+aba%20aba", chat_invite("aba%20aba")); + parse_internal_link("t.me/+aba%30aba", chat_invite("aba0aba")); + parse_internal_link("t.me/+123456a", chat_invite("123456a")); + parse_internal_link("t.me/%2012345678901", user_phone_number("12345678901")); + parse_internal_link("t.me/+123456", user_phone_number("123456")); + parse_internal_link("t.me/ 123456/123123/12/31/a/s//21w/?asdas#test", user_phone_number("123456")); + parse_internal_link("t.me/ /123456/123123/12/31/a/s//21w/?asdas#test", nullptr); + parse_internal_link("t.me/+123456?startattach", user_phone_number("123456")); + parse_internal_link("t.me/+123456?startattach=1", user_phone_number("123456")); + parse_internal_link("t.me/+123456?attach=", user_phone_number("123456")); + parse_internal_link("t.me/+123456?attach=&startattach", user_phone_number("123456")); + parse_internal_link("t.me/+123456?attach=&startattach=1", user_phone_number("123456")); + parse_internal_link("t.me/+123456?attach=bot", attachment_menu_bot(nullptr, user_phone_number("123456"), "bot", "")); + parse_internal_link("t.me/+123456?attach=bot&startattach", + attachment_menu_bot(nullptr, user_phone_number("123456"), "bot", "")); + parse_internal_link("t.me/+123456?attach=bot&startattach=1", + attachment_menu_bot(nullptr, user_phone_number("123456"), "bot", "1")); + + parse_internal_link("tg:join?invite=abcdef", chat_invite("abcdef")); + parse_internal_link("tg:join?invite=abc%20def", chat_invite("abc%20def")); + parse_internal_link("tg://join?invite=abc%30def", chat_invite("abc0def")); + parse_internal_link("tg:join?invite=", unknown_deep_link("tg://join?invite=")); + + parse_internal_link("t.me/addstickers?set=abcdef", nullptr); + parse_internal_link("t.me/addstickers", nullptr); + parse_internal_link("t.me/addstickers/", nullptr); + parse_internal_link("t.me/addstickers//abcdef", nullptr); + parse_internal_link("t.me/addstickers?/abcdef", nullptr); + parse_internal_link("t.me/addstickers/?abcdef", nullptr); + parse_internal_link("t.me/addstickers/#abcdef", nullptr); + parse_internal_link("t.me/addstickers/abacaba", sticker_set("abacaba")); + parse_internal_link("t.me/addstickers/aba%20aba", sticker_set("aba aba")); + parse_internal_link("t.me/addstickers/123456a", sticker_set("123456a")); + parse_internal_link("t.me/addstickers/12345678901", sticker_set("12345678901")); + parse_internal_link("t.me/addstickers/123456", sticker_set("123456")); + parse_internal_link("t.me/addstickers/123456/123123/12/31/a/s//21w/?asdas#test", sticker_set("123456")); + + parse_internal_link("tg:addstickers?set=abcdef", sticker_set("abcdef")); + parse_internal_link("tg:addstickers?set=abc%30ef", sticker_set("abc0ef")); + parse_internal_link("tg://addstickers?set=", unknown_deep_link("tg://addstickers?set=")); + + parse_internal_link("t.me/addemoji?set=abcdef", nullptr); + parse_internal_link("t.me/addemoji", nullptr); + parse_internal_link("t.me/addemoji/", nullptr); + parse_internal_link("t.me/addemoji//abcdef", nullptr); + parse_internal_link("t.me/addemoji?/abcdef", nullptr); + parse_internal_link("t.me/addemoji/?abcdef", nullptr); + parse_internal_link("t.me/addemoji/#abcdef", nullptr); + parse_internal_link("t.me/addemoji/abacaba", sticker_set("abacaba")); + parse_internal_link("t.me/addemoji/aba%20aba", sticker_set("aba aba")); + parse_internal_link("t.me/addemoji/123456a", sticker_set("123456a")); + parse_internal_link("t.me/addemoji/12345678901", sticker_set("12345678901")); + parse_internal_link("t.me/addemoji/123456", sticker_set("123456")); + parse_internal_link("t.me/addemoji/123456/123123/12/31/a/s//21w/?asdas#test", sticker_set("123456")); + + parse_internal_link("tg:addemoji?set=abcdef", sticker_set("abcdef")); + parse_internal_link("tg:addemoji?set=abc%30ef", sticker_set("abc0ef")); + parse_internal_link("tg://addemoji?set=", unknown_deep_link("tg://addemoji?set=")); + + parse_internal_link("t.me/confirmphone?hash=abc%30ef&phone=", nullptr); + parse_internal_link("t.me/confirmphone/123456/123123/12/31/a/s//21w/?hash=abc%30ef&phone=123456789", + phone_number_confirmation("abc0ef", "123456789")); + parse_internal_link("t.me/confirmphone?hash=abc%30ef&phone=123456789", + phone_number_confirmation("abc0ef", "123456789")); + + parse_internal_link("tg:confirmphone?hash=abc%30ef&phone=", + unknown_deep_link("tg://confirmphone?hash=abc%30ef&phone=")); + parse_internal_link("tg:confirmphone?hash=abc%30ef&phone=123456789", + phone_number_confirmation("abc0ef", "123456789")); + parse_internal_link("tg://confirmphone?hash=123&phone=123456789123456789", + phone_number_confirmation("123", "123456789123456789")); + parse_internal_link("tg://confirmphone?hash=&phone=123456789123456789", + unknown_deep_link("tg://confirmphone?hash=&phone=123456789123456789")); + parse_internal_link("tg://confirmphone?hash=123456789123456789&phone=", + unknown_deep_link("tg://confirmphone?hash=123456789123456789&phone=")); + + parse_internal_link("t.me/setlanguage?lang=abcdef", nullptr); + parse_internal_link("t.me/setlanguage", nullptr); + parse_internal_link("t.me/setlanguage/", nullptr); + parse_internal_link("t.me/setlanguage//abcdef", nullptr); + parse_internal_link("t.me/setlanguage?/abcdef", nullptr); + parse_internal_link("t.me/setlanguage/?abcdef", nullptr); + parse_internal_link("t.me/setlanguage/#abcdef", nullptr); + parse_internal_link("t.me/setlanguage/abacaba", language_pack("abacaba")); + parse_internal_link("t.me/setlanguage/aba%20aba", language_pack("aba aba")); + parse_internal_link("t.me/setlanguage/123456a", language_pack("123456a")); + parse_internal_link("t.me/setlanguage/12345678901", language_pack("12345678901")); + parse_internal_link("t.me/setlanguage/123456", language_pack("123456")); + parse_internal_link("t.me/setlanguage/123456/123123/12/31/a/s//21w/?asdas#test", language_pack("123456")); + + parse_internal_link("tg:setlanguage?lang=abcdef", language_pack("abcdef")); + parse_internal_link("tg:setlanguage?lang=abc%30ef", language_pack("abc0ef")); + parse_internal_link("tg://setlanguage?lang=", unknown_deep_link("tg://setlanguage?lang=")); + + parse_internal_link( + "http://telegram.dog/iv?url=https://telegram.org&rhash=abcdef&test=1&tg_rhash=1", + instant_view("https://t.me/iv?url=https%3A%2F%2Ftelegram.org&rhash=abcdef", "https://telegram.org")); + parse_internal_link("t.me/iva?url=https://telegram.org&rhash=abcdef", public_chat("iva")); + parse_internal_link("t.me/iv?url=&rhash=abcdef", nullptr); + parse_internal_link("t.me/iv?url=https://telegram.org&rhash=", + instant_view("https://t.me/iv?url=https%3A%2F%2Ftelegram.org&rhash", "https://telegram.org")); + parse_internal_link("t.me/iv//////?url=https://telegram.org&rhash=", + instant_view("https://t.me/iv?url=https%3A%2F%2Ftelegram.org&rhash", "https://telegram.org")); + parse_internal_link("t.me/iv/////1/?url=https://telegram.org&rhash=", nullptr); + parse_internal_link("t.me/iv", nullptr); + parse_internal_link("t.me/iv?#url=https://telegram.org&rhash=abcdef", nullptr); + parse_internal_link("tg:iv?url=https://telegram.org&rhash=abcdef", + unknown_deep_link("tg://iv?url=https://telegram.org&rhash=abcdef")); + + parse_internal_link("t.me/addtheme?slug=abcdef", nullptr); + parse_internal_link("t.me/addtheme", nullptr); + parse_internal_link("t.me/addtheme/", nullptr); + parse_internal_link("t.me/addtheme//abcdef", nullptr); + parse_internal_link("t.me/addtheme?/abcdef", nullptr); + parse_internal_link("t.me/addtheme/?abcdef", nullptr); + parse_internal_link("t.me/addtheme/#abcdef", nullptr); + parse_internal_link("t.me/addtheme/abacaba", theme("abacaba")); + parse_internal_link("t.me/addtheme/aba%20aba", theme("aba aba")); + parse_internal_link("t.me/addtheme/123456a", theme("123456a")); + parse_internal_link("t.me/addtheme/12345678901", theme("12345678901")); + parse_internal_link("t.me/addtheme/123456", theme("123456")); + parse_internal_link("t.me/addtheme/123456/123123/12/31/a/s//21w/?asdas#test", theme("123456")); + + parse_internal_link("tg:addtheme?slug=abcdef", theme("abcdef")); + parse_internal_link("tg:addtheme?slug=abc%30ef", theme("abc0ef")); + parse_internal_link("tg://addtheme?slug=", unknown_deep_link("tg://addtheme?slug=")); + + parse_internal_link("t.me/proxy?server=1.2.3.4&port=80&secret=1234567890abcdef1234567890ABCDEF", + proxy_mtproto("1.2.3.4", 80, "1234567890abcdef1234567890ABCDEF")); + parse_internal_link("t.me/proxy?server=1.2.3.4&port=80adasdas&secret=1234567890abcdef1234567890ABCDEF", + proxy_mtproto("1.2.3.4", 80, "1234567890abcdef1234567890ABCDEF")); + parse_internal_link("t.me/proxy?server=1.2.3.4&port=adasdas&secret=1234567890abcdef1234567890ABCDEF", + unsupported_proxy()); + parse_internal_link("t.me/proxy?server=1.2.3.4&port=65536&secret=1234567890abcdef1234567890ABCDEF", + unsupported_proxy()); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=", unsupported_proxy()); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=12", unsupported_proxy()); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=1234567890abcdef1234567890ABCDEF", + proxy_mtproto("google.com", 80, "1234567890abcdef1234567890ABCDEF")); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=dd1234567890abcdef1234567890ABCDEF", + proxy_mtproto("google.com", 80, "dd1234567890abcdef1234567890ABCDEF")); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=de1234567890abcdef1234567890ABCDEF", + unsupported_proxy()); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=ee1234567890abcdef1234567890ABCDEF", + unsupported_proxy()); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=ee1234567890abcdef1234567890ABCDEF0", + unsupported_proxy()); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=ee1234567890abcdef1234567890ABCDEF%30%30", + proxy_mtproto("google.com", 80, "ee1234567890abcdef1234567890ABCDEF00")); + parse_internal_link( + "t.me/proxy?server=google.com&port=8%30&secret=ee1234567890abcdef1234567890ABCDEF010101010101010101", + proxy_mtproto("google.com", 80, "ee1234567890abcdef1234567890ABCDEF010101010101010101")); + parse_internal_link("t.me/proxy?server=google.com&port=8%30&secret=7tAAAAAAAAAAAAAAAAAAAAAAAAcuZ29vZ2xlLmNvbQ", + proxy_mtproto("google.com", 80, "7tAAAAAAAAAAAAAAAAAAAAAAAAcuZ29vZ2xlLmNvbQ")); + + parse_internal_link("tg:proxy?server=1.2.3.4&port=80&secret=1234567890abcdef1234567890ABCDEF", + proxy_mtproto("1.2.3.4", 80, "1234567890abcdef1234567890ABCDEF")); + parse_internal_link("tg:proxy?server=1.2.3.4&port=80adasdas&secret=1234567890abcdef1234567890ABCDEF", + proxy_mtproto("1.2.3.4", 80, "1234567890abcdef1234567890ABCDEF")); + parse_internal_link("tg:proxy?server=1.2.3.4&port=adasdas&secret=1234567890abcdef1234567890ABCDEF", + unsupported_proxy()); + parse_internal_link("tg:proxy?server=1.2.3.4&port=65536&secret=1234567890abcdef1234567890ABCDEF", + unsupported_proxy()); + parse_internal_link("tg:proxy?server=google.com&port=8%30&secret=1234567890abcdef1234567890ABCDEF", + proxy_mtproto("google.com", 80, "1234567890abcdef1234567890ABCDEF")); + parse_internal_link("tg:proxy?server=google.com&port=8%30&secret=dd1234567890abcdef1234567890ABCDEF", + proxy_mtproto("google.com", 80, "dd1234567890abcdef1234567890ABCDEF")); + parse_internal_link("tg:proxy?server=google.com&port=8%30&secret=de1234567890abcdef1234567890ABCDEF", + unsupported_proxy()); + + parse_internal_link("t.me/socks?server=1.2.3.4&port=80", proxy_socks("1.2.3.4", 80, "", "")); + parse_internal_link("t.me/socks?server=1.2.3.4&port=80adasdas", proxy_socks("1.2.3.4", 80, "", "")); + parse_internal_link("t.me/socks?server=1.2.3.4&port=adasdas", unsupported_proxy()); + parse_internal_link("t.me/socks?server=1.2.3.4&port=65536", unsupported_proxy()); + parse_internal_link("t.me/socks?server=google.com&port=8%30", proxy_socks("google.com", 80, "", "")); + parse_internal_link("t.me/socks?server=google.com&port=8%30&user=1&pass=", proxy_socks("google.com", 80, "1", "")); + parse_internal_link("t.me/socks?server=google.com&port=8%30&user=&pass=2", proxy_socks("google.com", 80, "", "2")); + parse_internal_link("t.me/socks?server=google.com&port=80&user=1&pass=2", proxy_socks("google.com", 80, "1", "2")); + + parse_internal_link("tg:socks?server=1.2.3.4&port=80", proxy_socks("1.2.3.4", 80, "", "")); + parse_internal_link("tg:socks?server=1.2.3.4&port=80adasdas", proxy_socks("1.2.3.4", 80, "", "")); + parse_internal_link("tg:socks?server=1.2.3.4&port=adasdas", unsupported_proxy()); + parse_internal_link("tg:socks?server=1.2.3.4&port=65536", unsupported_proxy()); + parse_internal_link("tg:socks?server=google.com&port=8%30", proxy_socks("google.com", 80, "", "")); + parse_internal_link("tg:socks?server=google.com&port=8%30&user=1&pass=", proxy_socks("google.com", 80, "1", "")); + parse_internal_link("tg:socks?server=google.com&port=8%30&user=&pass=2", proxy_socks("google.com", 80, "", "2")); + parse_internal_link("tg:socks?server=google.com&port=80&user=1&pass=2", proxy_socks("google.com", 80, "1", "2")); + + parse_internal_link("tg:resolve?domain=username&voice%63hat=aasdasd", video_chat("username", "aasdasd", false)); + parse_internal_link("tg:resolve?domain=username&video%63hat=aasdasd", video_chat("username", "aasdasd", false)); + parse_internal_link("tg:resolve?domain=username&livestream=aasdasd", video_chat("username", "aasdasd", true)); + parse_internal_link("TG://resolve?domain=username&voicechat=", video_chat("username", "", false)); + parse_internal_link("TG://test@resolve?domain=username&voicechat=", nullptr); + parse_internal_link("tg:resolve:80?domain=username&voicechat=", nullptr); + parse_internal_link("tg:http://resolve?domain=username&voicechat=", nullptr); + parse_internal_link("tg:https://resolve?domain=username&voicechat=", nullptr); + parse_internal_link("tg:resolve?domain=&voicechat=", unknown_deep_link("tg://resolve?domain=&voicechat=")); + parse_internal_link("tg:resolve?domain=telegram&&&&&&&voicechat=%30", video_chat("telegram", "0", false)); + + parse_internal_link("t.me/username/0/a//s/as?voicechat=", video_chat("username", "", false)); + parse_internal_link("t.me/username/0/a//s/as?videochat=2", video_chat("username", "2", false)); + parse_internal_link("t.me/username/0/a//s/as?livestream=3", video_chat("username", "3", true)); + parse_internal_link("t.me/username/aasdas?test=1&voicechat=#12312", video_chat("username", "", false)); + parse_internal_link("t.me/username/0?voicechat=", video_chat("username", "", false)); + parse_internal_link("t.me/username/-1?voicechat=asdasd", video_chat("username", "asdasd", false)); + parse_internal_link("t.me/username?voicechat=", video_chat("username", "", false)); + parse_internal_link("t.me/username#voicechat=asdas", public_chat("username")); + parse_internal_link("t.me//username?voicechat=", nullptr); + parse_internal_link("https://telegram.dog/tele%63ram?voi%63e%63hat=t%63st", video_chat("telecram", "tcst", false)); + + parse_internal_link("tg:resolve?domain=username&start=aasdasd", bot_start("username", "aasdasd")); + parse_internal_link("TG://resolve?domain=username&start=", bot_start("username", "")); + parse_internal_link("TG://test@resolve?domain=username&start=", nullptr); + parse_internal_link("tg:resolve:80?domain=username&start=", nullptr); + parse_internal_link("tg:http://resolve?domain=username&start=", nullptr); + parse_internal_link("tg:https://resolve?domain=username&start=", nullptr); + parse_internal_link("tg:resolve?domain=&start=", unknown_deep_link("tg://resolve?domain=&start=")); + parse_internal_link("tg:resolve?domain=telegram&&&&&&&start=%30", bot_start("telegram", "0")); + + parse_internal_link("t.me/username/0/a//s/as?start=", bot_start("username", "")); + parse_internal_link("t.me/username/aasdas?test=1&start=#12312", bot_start("username", "")); + parse_internal_link("t.me/username/0?start=", bot_start("username", "")); + parse_internal_link("t.me/username/-1?start=asdasd", bot_start("username", "asdasd")); + parse_internal_link("t.me/username?start=", bot_start("username", "")); + parse_internal_link("t.me/username#start=asdas", public_chat("username")); + parse_internal_link("t.me//username?start=", nullptr); + parse_internal_link("https://telegram.dog/tele%63ram?start=t%63st", bot_start("telecram", "tcst")); + + parse_internal_link("tg:resolve?domain=username&startgroup=aasdasd", + bot_start_in_group("username", "aasdasd", nullptr)); + parse_internal_link("TG://resolve?domain=username&startgroup=", bot_start_in_group("username", "", nullptr)); + parse_internal_link("TG://test@resolve?domain=username&startgroup=", nullptr); + parse_internal_link("tg:resolve:80?domain=username&startgroup=", nullptr); + parse_internal_link("tg:http://resolve?domain=username&startgroup=", nullptr); + parse_internal_link("tg:https://resolve?domain=username&startgroup=", nullptr); + parse_internal_link("tg:resolve?domain=&startgroup=", unknown_deep_link("tg://resolve?domain=&startgroup=")); + parse_internal_link("tg:resolve?domain=telegram&&&&&&&startgroup=%30", bot_start_in_group("telegram", "0", nullptr)); + + parse_internal_link("tg:resolve?domain=username&startgroup", bot_start_in_group("username", "", nullptr)); + parse_internal_link("tg:resolve?domain=username&startgroup&admin=asdas", bot_start_in_group("username", "", nullptr)); + parse_internal_link("tg:resolve?domain=username&startgroup&admin=post_messages", + bot_start_in_group("username", "", nullptr)); + parse_internal_link("tg:resolve?domain=username&startgroup=1&admin=delete_messages+anonymous", + bot_start_in_group("username", "1", + chat_administrator_rights(true, false, false, false, true, false, false, false, + false, false, false, true))); + parse_internal_link( + "tg:resolve?domain=username&startgroup&admin=manage_chat+change_info+post_messages+edit_messages+delete_messages+" + "invite_users+restrict_members+pin_messages+manage_topics+promote_members+manage_video_chats+anonymous", + bot_start_in_group( + "username", "", + chat_administrator_rights(true, true, false, false, true, true, true, true, true, true, true, true))); + + parse_internal_link("tg:resolve?domain=username&startchannel", public_chat("username")); + parse_internal_link("tg:resolve?domain=username&startchannel&admin=", public_chat("username")); + parse_internal_link( + "tg:resolve?domain=username&startchannel&admin=post_messages", + bot_add_to_channel("username", chat_administrator_rights(true, false, true, false, false, false, true, false, + false, false, false, false))); + parse_internal_link( + "tg:resolve?domain=username&startchannel&admin=manage_chat+change_info+post_messages+edit_messages+delete_" + "messages+invite_users+restrict_members+pin_messages+manage_topics+promote_members+manage_video_chats+anonymous", + bot_add_to_channel("username", chat_administrator_rights(true, true, true, true, true, true, true, false, false, + true, true, false))); + + parse_internal_link("t.me/username/0/a//s/as?startgroup=", bot_start_in_group("username", "", nullptr)); + parse_internal_link("t.me/username/aasdas?test=1&startgroup=#12312", bot_start_in_group("username", "", nullptr)); + parse_internal_link("t.me/username/0?startgroup=", bot_start_in_group("username", "", nullptr)); + parse_internal_link("t.me/username/-1?startgroup=asdasd", bot_start_in_group("username", "asdasd", nullptr)); + parse_internal_link("t.me/username?startgroup=", bot_start_in_group("username", "", nullptr)); + parse_internal_link("t.me/username#startgroup=asdas", public_chat("username")); + parse_internal_link("t.me//username?startgroup=", nullptr); + parse_internal_link("https://telegram.dog/tele%63ram?startgroup=t%63st", + bot_start_in_group("telecram", "tcst", nullptr)); + + parse_internal_link("t.me/username?startgroup", bot_start_in_group("username", "", nullptr)); + parse_internal_link("t.me/username?startgroup&admin=asdas", bot_start_in_group("username", "", nullptr)); + parse_internal_link("t.me/username?startgroup&admin=post_messages", bot_start_in_group("username", "", nullptr)); + parse_internal_link("t.me/username?startgroup=1&admin=delete_messages+anonymous", + bot_start_in_group("username", "1", + chat_administrator_rights(true, false, false, false, true, false, false, false, + false, false, false, true))); + parse_internal_link( + "t.me/" + "username?startgroup&admin=manage_chat+change_info+post_messages+edit_messages+delete_messages+invite_users+" + "restrict_members+pin_messages+manage_topics+promote_members+manage_video_chats+anonymous", + bot_start_in_group( + "username", "", + chat_administrator_rights(true, true, false, false, true, true, true, true, true, true, true, true))); + + parse_internal_link("t.me/username?startchannel", public_chat("username")); + parse_internal_link("t.me/username?startchannel&admin=", public_chat("username")); + parse_internal_link( + "t.me/username?startchannel&admin=post_messages", + bot_add_to_channel("username", chat_administrator_rights(true, false, true, false, false, false, true, false, + false, false, false, false))); + parse_internal_link( + "t.me/" + "username?startchannel&admin=manage_chat+change_info+post_messages+edit_messages+delete_messages+invite_users+" + "restrict_members+pin_messages+manage_topics+promote_members+manage_video_chats+anonymous", + bot_add_to_channel("username", chat_administrator_rights(true, true, true, true, true, true, true, false, false, + true, true, false))); + + parse_internal_link("tg:resolve?domain=username&game=aasdasd", game("username", "aasdasd")); + parse_internal_link("TG://resolve?domain=username&game=", public_chat("username")); + parse_internal_link("TG://test@resolve?domain=username&game=asd", nullptr); + parse_internal_link("tg:resolve:80?domain=username&game=asd", nullptr); + parse_internal_link("tg:http://resolve?domain=username&game=asd", nullptr); + parse_internal_link("tg:https://resolve?domain=username&game=asd", nullptr); + parse_internal_link("tg:resolve?domain=&game=asd", unknown_deep_link("tg://resolve?domain=&game=asd")); + parse_internal_link("tg:resolve?domain=telegram&&&&&&&game=%30", game("telegram", "0")); + + parse_internal_link("t.me/username/0/a//s/as?game=asd", game("username", "asd")); + parse_internal_link("t.me/username/aasdas?test=1&game=asd#12312", game("username", "asd")); + parse_internal_link("t.me/username/0?game=asd", game("username", "asd")); + parse_internal_link("t.me/username/-1?game=asdasd", game("username", "asdasd")); + parse_internal_link("t.me/username?game=asd", game("username", "asd")); + parse_internal_link("t.me/username?game=", public_chat("username")); + parse_internal_link("t.me/username#game=asdas", public_chat("username")); + parse_internal_link("t.me//username?game=asd", nullptr); + parse_internal_link("https://telegram.dog/tele%63ram?game=t%63st", game("telecram", "tcst")); + + parse_internal_link("tg:resolve?domain=username&Game=asd", public_chat("username")); + parse_internal_link("TG://test@resolve?domain=username", nullptr); + parse_internal_link("tg:resolve:80?domain=username", nullptr); + parse_internal_link("tg:http://resolve?domain=username", nullptr); + parse_internal_link("tg:https://resolve?domain=username", nullptr); + parse_internal_link("tg:resolve?domain=", unknown_deep_link("tg://resolve?domain=")); + parse_internal_link("tg:resolve?&&&&&&&domain=telegram", public_chat("telegram")); + + parse_internal_link("t.me/a", public_chat("a")); + parse_internal_link("t.me/abcdefghijklmnopqrstuvwxyz123456", public_chat("abcdefghijklmnopqrstuvwxyz123456")); + parse_internal_link("t.me/abcdefghijklmnopqrstuvwxyz1234567", nullptr); + parse_internal_link("t.me/abcdefghijklmnop-qrstuvwxyz", nullptr); + parse_internal_link("t.me/abcdefghijklmnop~qrstuvwxyz", nullptr); + parse_internal_link("t.me/_asdf", nullptr); + parse_internal_link("t.me/0asdf", nullptr); + parse_internal_link("t.me/9asdf", nullptr); + parse_internal_link("t.me/Aasdf", public_chat("Aasdf")); + parse_internal_link("t.me/asdf_", nullptr); + parse_internal_link("t.me/asdf0", public_chat("asdf0")); + parse_internal_link("t.me/asd__fg", nullptr); + parse_internal_link("t.me/username/0/a//s/as?gam=asd", public_chat("username")); + parse_internal_link("t.me/username/aasdas?test=1", public_chat("username")); + parse_internal_link("t.me/username/0", public_chat("username")); + parse_internal_link("t.me//username", nullptr); + parse_internal_link("https://telegram.dog/tele%63ram", public_chat("telecram")); + + parse_internal_link( + "tg://" + "resolve?domain=telegrampassport&bot_id=543260180&scope=%7B%22v%22%3A1%2C%22d%22%3A%5B%7B%22%22%5D%7D%5D%7D&" + "public_key=BEGIN%20PUBLIC%20KEY%0A&nonce=b8ee&callback_url=https%3A%2F%2Fcore.telegram.org%2Fpassport%2Fexample%" + "3Fpassport_ssid%3Db8ee&payload=nonce", + passport_data_request(543260180, "{\"v\":1,\"d\":[{\"\"]}]}", "BEGIN PUBLIC KEY\n", "b8ee", + "https://core.telegram.org/passport/example?passport_ssid=b8ee")); + parse_internal_link("tg://resolve?domain=telegrampassport&bot_id=12345&public_key=key&scope=asd&payload=nonce", + passport_data_request(12345, "asd", "key", "nonce", "")); + parse_internal_link("tg://passport?bot_id=12345&public_key=key&scope=asd&payload=nonce", + passport_data_request(12345, "asd", "key", "nonce", "")); + parse_internal_link("tg://passport?bot_id=0&public_key=key&scope=asd&payload=nonce", + unknown_deep_link("tg://passport?bot_id=0&public_key=key&scope=asd&payload=nonce")); + parse_internal_link("tg://passport?bot_id=-1&public_key=key&scope=asd&payload=nonce", + unknown_deep_link("tg://passport?bot_id=-1&public_key=key&scope=asd&payload=nonce")); + parse_internal_link("tg://passport?bot_id=12345&public_key=&scope=asd&payload=nonce", + unknown_deep_link("tg://passport?bot_id=12345&public_key=&scope=asd&payload=nonce")); + parse_internal_link("tg://passport?bot_id=12345&public_key=key&scope=&payload=nonce", + unknown_deep_link("tg://passport?bot_id=12345&public_key=key&scope=&payload=nonce")); + parse_internal_link("tg://passport?bot_id=12345&public_key=key&scope=asd&payload=", + unknown_deep_link("tg://passport?bot_id=12345&public_key=key&scope=asd&payload=")); + parse_internal_link("t.me/telegrampassport?bot_id=12345&public_key=key&scope=asd&payload=nonce", + public_chat("telegrampassport")); + + parse_internal_link("tg:premium_offer?ref=abcdef", premium_features("abcdef")); + parse_internal_link("tg:premium_offer?ref=abc%30ef", premium_features("abc0ef")); + parse_internal_link("tg://premium_offer?ref=", premium_features("")); + + parse_internal_link("tg://settings", settings()); + parse_internal_link("tg://setting", unknown_deep_link("tg://setting")); + parse_internal_link("tg://settings?asdsa?D?SADasD?asD", settings()); + parse_internal_link("tg://settings#test", settings()); + parse_internal_link("tg://settings/#test", settings()); + parse_internal_link("tg://settings/aadsa#test", settings()); + parse_internal_link("tg://settings/theme#test", settings()); + parse_internal_link("tg://settings/themes#test", theme_settings()); + parse_internal_link("tg://settings/themesa#test", settings()); + parse_internal_link("tg://settings/themes/?as#rad", theme_settings()); + parse_internal_link("tg://settings/themes/a", settings()); + parse_internal_link("tg://settings/asdsathemesasdas/devices", settings()); + parse_internal_link("tg://settings/devices", active_sessions()); + parse_internal_link("tg://settings/change_number", change_phone_number()); + parse_internal_link("tg://settings/folders", filter_settings()); + parse_internal_link("tg://settings/filters", settings()); + parse_internal_link("tg://settings/language", language_settings()); + parse_internal_link("tg://settings/privacy", privacy_and_security_settings()); + + parse_internal_link("username.t.me////0/a//s/as?start=", bot_start("username", "")); + parse_internal_link("username.t.me?start=as", bot_start("username", "as")); + parse_internal_link("username.t.me", public_chat("username")); + parse_internal_link("aAAb.t.me/12345?single", message("tg:resolve?domain=aaab&post=12345&single")); + parse_internal_link("telegram.t.me/195", message("tg:resolve?domain=telegram&post=195")); + parse_internal_link("shares.t.me", public_chat("shares")); + + parse_internal_link("c.t.me/12345?single", nullptr); + parse_internal_link("aaa.t.me/12345?single", nullptr); + parse_internal_link("aaa_.t.me/12345?single", nullptr); + parse_internal_link("0aaa.t.me/12345?single", nullptr); + parse_internal_link("_aaa.t.me/12345?single", nullptr); + parse_internal_link("addemoji.t.me", nullptr); + parse_internal_link("addstickers.t.me", nullptr); + parse_internal_link("addtheme.t.me", nullptr); + parse_internal_link("auth.t.me", nullptr); + parse_internal_link("confirmphone.t.me", nullptr); + parse_internal_link("invoice.t.me", nullptr); + parse_internal_link("joinchat.t.me", nullptr); + parse_internal_link("login.t.me", nullptr); + parse_internal_link("proxy.t.me", nullptr); + parse_internal_link("setlanguage.t.me", nullptr); + parse_internal_link("share.t.me", nullptr); + parse_internal_link("socks.t.me", nullptr); + + parse_internal_link("www.telegra.ph/", nullptr); + parse_internal_link("www.telegrA.ph/#", nullptr); + parse_internal_link("www.telegrA.ph/?", instant_view("https://telegra.ph/?", "www.telegrA.ph/?")); + parse_internal_link("http://te.leGra.ph/?", instant_view("https://telegra.ph/?", "http://te.leGra.ph/?")); + parse_internal_link("https://grAph.org/12345", instant_view("https://telegra.ph/12345", "https://grAph.org/12345")); +} diff --git a/protocols/Telegram/tdlib/td/test/main.cpp b/protocols/Telegram/tdlib/td/test/main.cpp index 0ef46c75b2..064871ae31 100644 --- a/protocols/Telegram/tdlib/td/test/main.cpp +++ b/protocols/Telegram/tdlib/td/test/main.cpp @@ -1,40 +1,64 @@ // -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include "td/utils/tests.h" - +#include "td/utils/common.h" +#include "td/utils/crypto.h" +#include "td/utils/ExitGuard.h" #include "td/utils/logging.h" - -#include +#include "td/utils/OptionParser.h" +#include "td/utils/port/detail/ThreadIdGuard.h" +#include "td/utils/port/stacktrace.h" +#include "td/utils/Slice.h" +#include "td/utils/Status.h" +#include "td/utils/tests.h" #if TD_EMSCRIPTEN #include #endif int main(int argc, char **argv) { - // TODO port OptionsParser to Windows - for (int i = 1; i < argc; i++) { - if (!std::strcmp(argv[i], "--filter")) { - CHECK(i + 1 < argc); - td::Test::add_substr_filter(argv[++i]); - } - if (!std::strcmp(argv[i], "--stress")) { - td::Test::set_stress_flag(true); + SET_VERBOSITY_LEVEL(VERBOSITY_NAME(FATAL)); + td::ExitGuard exit_guard; + td::detail::ThreadIdGuard thread_id_guard; + td::Stacktrace::init(); + td::init_openssl_threads(); + + td::TestsRunner &runner = td::TestsRunner::get_default(); + + int default_verbosity_level = 1; + td::OptionParser options; + options.add_option('f', "filter", "run only specified tests", + [&](td::Slice filter) { runner.add_substr_filter(filter.str()); }); + options.add_option('s', "stress", "run tests infinitely", [&] { runner.set_stress_flag(true); }); + options.add_checked_option('v', "verbosity", "log verbosity level", + td::OptionParser::parse_integer(default_verbosity_level)); + options.add_check([&] { + if (default_verbosity_level < 0) { + return td::Status::Error("Wrong verbosity level specified"); } + return td::Status::OK(); + }); + auto r_non_options = options.run(argc, argv, 0); + if (r_non_options.is_error()) { + LOG(PLAIN) << argv[0] << ": " << r_non_options.error().message(); + LOG(PLAIN) << options; + return 1; } + SET_VERBOSITY_LEVEL(default_verbosity_level); + #if TD_EMSCRIPTEN emscripten_set_main_loop( [] { - if (!td::Test::run_all_step()) { + td::TestsRunner &default_runner = td::TestsRunner::get_default(); + if (!default_runner.run_all_step()) { emscripten_cancel_main_loop(); } }, 10, 0); #else - td::Test::run_all(); + runner.run_all(); #endif - return 0; } diff --git a/protocols/Telegram/tdlib/td/test/message_entities.cpp b/protocols/Telegram/tdlib/td/test/message_entities.cpp index 473a87140a..9c62415579 100644 --- a/protocols/Telegram/tdlib/td/test/message_entities.cpp +++ b/protocols/Telegram/tdlib/td/test/message_entities.cpp @@ -1,28 +1,36 @@ // -// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include "td/telegram/CustomEmojiId.h" #include "td/telegram/MessageEntity.h" +#include "td/telegram/UserId.h" +#include "td/utils/algorithm.h" +#include "td/utils/common.h" #include "td/utils/format.h" #include "td/utils/logging.h" +#include "td/utils/misc.h" +#include "td/utils/Random.h" +#include "td/utils/Slice.h" +#include "td/utils/SliceBuilder.h" #include "td/utils/tests.h" +#include "td/utils/utf8.h" -REGISTER_TESTS(message_entities); +#include +#include -using namespace td; - -static void check_mention(string str, std::vector expected) { - auto result_slice = find_mentions(str); - std::vector result; +static void check_mention(const td::string &str, const td::vector &expected) { + auto result_slice = td::find_mentions(str); + td::vector result; for (auto &it : result_slice) { result.push_back(it.str()); } if (result != expected) { - LOG(FATAL) << tag("text", str) << tag("got", format::as_array(result)) - << tag("expected", format::as_array(expected)); + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result)) + << td::tag("expected", td::format::as_array(expected)); } } @@ -39,20 +47,21 @@ TEST(MessageEntities, mention) { check_mention("@abcdefghijklmnopqrstuvwxyz123456", {"@abcdefghijklmnopqrstuvwxyz123456"}); check_mention("@abcdefghijklmnopqrstuvwxyz1234567", {}); check_mention("нет@mention", {}); - check_mention("@ya @gif @wiki @vid @bing @pic @bold @imdb @coub @like @vote @giff @cap ya cap @y @yar @bingg @bin", - {"@ya", "@gif", "@wiki", "@vid", "@bing", "@pic", "@bold", "@imdb", "@coub", "@like", "@vote", "@giff", - "@cap", "@bingg"}); -}; - -static void check_bot_command(string str, std::vector expected) { - auto result_slice = find_bot_commands(str); - std::vector result; + check_mention( + "@ya @gif @wiki @vid @bing @pic @bold @imdb @ImDb @coub @like @vote @giff @cap ya cap @y @yar @bingg @bin", + {"@gif", "@wiki", "@vid", "@bing", "@pic", "@bold", "@imdb", "@ImDb", "@coub", "@like", "@vote", "@giff", + "@bingg"}); +} + +static void check_bot_command(const td::string &str, const td::vector &expected) { + auto result_slice = td::find_bot_commands(str); + td::vector result; for (auto &it : result_slice) { result.push_back(it.str()); } if (result != expected) { - LOG(FATAL) << tag("text", str) << tag("got", format::as_array(result)) - << tag("expected", format::as_array(expected)); + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result)) + << td::tag("expected", td::format::as_array(expected)); } } @@ -68,15 +77,15 @@ TEST(MessageEntities, bot_command) { check_bot_command("/test/", {}); } -static void check_hashtag(string str, std::vector expected) { - auto result_slice = find_hashtags(str); - std::vector result; +static void check_hashtag(const td::string &str, const td::vector &expected) { + auto result_slice = td::find_hashtags(str); + td::vector result; for (auto &it : result_slice) { result.push_back(it.str()); } if (result != expected) { - LOG(FATAL) << tag("text", str) << tag("got", format::as_array(result)) - << tag("expected", format::as_array(expected)); + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result)) + << td::tag("expected", td::format::as_array(expected)); } } @@ -95,29 +104,32 @@ TEST(MessageEntities, hashtag) { check_hashtag(" #123a ", {"#123a"}); check_hashtag(" #a123 ", {"#a123"}); check_hashtag(" #123a# ", {}); - check_hashtag(" #" + string(300, '1'), {}); - check_hashtag(" #" + string(256, '1'), {}); - check_hashtag(" #" + string(256, '1') + "a ", {}); - check_hashtag(" #" + string(255, '1') + "a", {"#" + string(255, '1') + "a"}); - check_hashtag(" #" + string(255, '1') + "Я", {"#" + string(255, '1') + "Я"}); - check_hashtag(" #" + string(255, '1') + "a" + string(255, 'b') + "# ", {}); + check_hashtag(" #" + td::string(300, '1'), {}); + check_hashtag(" #" + td::string(256, '1'), {}); + check_hashtag(" #" + td::string(256, '1') + "a ", {}); + check_hashtag(" #" + td::string(255, '1') + "a", {"#" + td::string(255, '1') + "a"}); + check_hashtag(" #" + td::string(255, '1') + "Я", {"#" + td::string(255, '1') + "Я"}); + check_hashtag(" #" + td::string(255, '1') + "a" + td::string(255, 'b') + "# ", {}); check_hashtag("#a#b #c #d", {"#c", "#d"}); check_hashtag("#test", {"#test"}); - check_hashtag(u8"\U0001F604\U0001F604\U0001F604\U0001F604 \U0001F604\U0001F604\U0001F604#" + string(200, '1') + - "ООО" + string(200, '2'), - {"#" + string(200, '1') + "ООО" + string(53, '2')}); + check_hashtag("#te·st", {"#te·st"}); + check_hashtag(u8"\U0001F604\U0001F604\U0001F604\U0001F604 \U0001F604\U0001F604\U0001F604#" + td::string(200, '1') + + "ООО" + td::string(200, '2'), + {"#" + td::string(200, '1') + "ООО" + td::string(53, '2')}); check_hashtag(u8"#a\u2122", {"#a"}); + check_hashtag("#a൹", {"#a"}); + check_hashtag("#aඁං෴ก฿", {"#aඁං෴ก"}); } -static void check_cashtag(string str, std::vector expected) { - auto result_slice = find_cashtags(str); - std::vector result; +static void check_cashtag(const td::string &str, const td::vector &expected) { + auto result_slice = td::find_cashtags(str); + td::vector result; for (auto &it : result_slice) { result.push_back(it.str()); } if (result != expected) { - LOG(FATAL) << tag("text", str) << tag("got", format::as_array(result)) - << tag("expected", format::as_array(expected)); + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result)) + << td::tag("expected", td::format::as_array(expected)); } } @@ -133,8 +145,9 @@ TEST(MessageEntities, cashtag) { check_cashtag("$ab", {}); check_cashtag("$abc", {}); check_cashtag("$", {}); - check_cashtag("$A", {}); - check_cashtag("$AB", {}); + check_cashtag("$A", {"$A"}); + check_cashtag("$AB", {"$AB"}); + check_cashtag("$ABС", {}); check_cashtag("$АBC", {}); check_cashtag("$АВС", {}); check_cashtag("$ABC", {"$ABC"}); @@ -156,13 +169,172 @@ TEST(MessageEntities, cashtag) { check_cashtag(" А$ABC ", {}); check_cashtag("$ABC$DEF $GHI $KLM", {"$GHI", "$KLM"}); check_cashtag("$TEST", {"$TEST"}); + check_cashtag("$1INC", {}); + check_cashtag("$1INCH", {"$1INCH"}); + check_cashtag("...$1INCH...", {"$1INCH"}); + check_cashtag("$1inch", {}); + check_cashtag("$1INCHA", {}); + check_cashtag("$1INCHА", {}); check_cashtag(u8"$ABC\u2122", {"$ABC"}); check_cashtag(u8"\u2122$ABC", {"$ABC"}); check_cashtag(u8"\u2122$ABC\u2122", {"$ABC"}); + check_cashtag("$ABC൹", {"$ABC"}); + check_cashtag("$ABCඁ", {}); + check_cashtag("$ABCං", {}); + check_cashtag("$ABC෴", {}); + check_cashtag("$ABCก", {}); + check_cashtag("$ABC฿", {"$ABC"}); +} + +static void check_media_timestamp(const td::string &str, const td::vector> &expected) { + auto result = td::transform(td::find_media_timestamps(str), + [](auto &&entity) { return std::make_pair(entity.first.str(), entity.second); }); + if (result != expected) { + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result)) + << td::tag("expected", td::format::as_array(expected)); + } +} + +TEST(MessageEntities, media_timestamp) { + check_media_timestamp("", {}); + check_media_timestamp(":", {}); + check_media_timestamp(":1", {}); + check_media_timestamp("a:1", {}); + check_media_timestamp("01", {}); + check_media_timestamp("01:", {}); + check_media_timestamp("01::", {}); + check_media_timestamp("01::", {}); + check_media_timestamp("a1:1a", {}); + check_media_timestamp("a1::01a", {}); + check_media_timestamp("2001:db8::8a2e:f70:13a4", {}); + check_media_timestamp("0:00", {{"0:00", 0}}); + check_media_timestamp("+0:00", {{"0:00", 0}}); + check_media_timestamp("0:00+", {{"0:00", 0}}); + check_media_timestamp("a0:00", {}); + check_media_timestamp("0:00a", {}); + check_media_timestamp("б0:00", {}); + check_media_timestamp("0:00б", {}); + check_media_timestamp("_0:00", {}); + check_media_timestamp("0:00_", {}); + check_media_timestamp("00:00:00:00", {}); + check_media_timestamp("1:1:01 1:1:1", {{"1:1:01", 3661}}); + check_media_timestamp("0:0:00 00:00 000:00 0000:00 00000:00 00:00:00 000:00:00 00:000:00 00:00:000", + {{"0:0:00", 0}, {"00:00", 0}, {"000:00", 0}, {"0000:00", 0}, {"00:00:00", 0}}); + check_media_timestamp("00:0:00 0:00:00 00::00 :00:00 00:00: 00:00:0 00:00:", {{"00:0:00", 0}, {"0:00:00", 0}}); + check_media_timestamp("1:1:59 1:1:-1 1:1:60", {{"1:1:59", 3719}}); + check_media_timestamp("1:59:00 1:-1:00 1:60:00", {{"1:59:00", 7140}, {"1:00", 60}}); + check_media_timestamp("59:59 60:00", {{"59:59", 3599}, {"60:00", 3600}}); + check_media_timestamp("9999:59 99:59:59 99:60:59", {{"9999:59", 599999}, {"99:59:59", 360000 - 1}}); + check_media_timestamp("2001:db8::8a2e:f70:13a4", {}); +} + +static void check_bank_card_number(const td::string &str, const td::vector &expected) { + auto result_slice = td::find_bank_card_numbers(str); + td::vector result; + for (auto &it : result_slice) { + result.push_back(it.str()); + } + if (result != expected) { + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result)) + << td::tag("expected", td::format::as_array(expected)); + } +} + +TEST(MessageEntities, bank_card_number) { + check_bank_card_number("", {}); + check_bank_card_number("123456789015", {}); + check_bank_card_number("1234567890120", {}); + check_bank_card_number("1234567890121", {}); + check_bank_card_number("1234567890122", {}); + check_bank_card_number("1234567890123", {}); + check_bank_card_number("1234567890124", {}); + check_bank_card_number("1234567890125", {}); + check_bank_card_number("1234567890126", {}); + check_bank_card_number("1234567890127", {}); + check_bank_card_number("1234567890128", {"1234567890128"}); + check_bank_card_number("1234567890129", {}); + check_bank_card_number("12345678901500", {"12345678901500"}); + check_bank_card_number("123456789012800", {"123456789012800"}); + check_bank_card_number("1234567890151800", {"1234567890151800"}); + check_bank_card_number("12345678901280000", {"12345678901280000"}); + check_bank_card_number("123456789015009100", {"123456789015009100"}); + check_bank_card_number("1234567890128000000", {"1234567890128000000"}); + check_bank_card_number("12345678901500910000", {}); + check_bank_card_number(" - - - - 1 - -- 2 - - -- 34 - - - 56- - 7890150000 - - - -", {}); + check_bank_card_number(" - - - - 1 - -- 234 - - 56- - 7890150000 - - - -", {"1 - -- 234 - - 56- - 7890150000"}); + check_bank_card_number("4916-3385-0608-2832; 5280 9342 8317 1080 ;345936346788903", + {"4916-3385-0608-2832", "5280 9342 8317 1080", "345936346788903"}); + check_bank_card_number("4556728228023269, 4916141675244747020, 49161416752447470, 4556728228023269", + {"4556728228023269", "4916141675244747020", "4556728228023269"}); + check_bank_card_number("a1234567890128", {}); + check_bank_card_number("1234567890128a", {}); + check_bank_card_number("1234567890128а", {}); + check_bank_card_number("а1234567890128", {}); + check_bank_card_number("1234567890128_", {}); + check_bank_card_number("_1234567890128", {}); + check_bank_card_number("1234567890128/", {"1234567890128"}); + check_bank_card_number("\"1234567890128", {"1234567890128"}); + check_bank_card_number("+1234567890128", {}); } -static void check_is_email_address(string str, bool expected) { - bool result = is_email_address(str); +static void check_tg_url(const td::string &str, const td::vector &expected) { + auto result_slice = td::find_tg_urls(str); + td::vector result; + for (auto &it : result_slice) { + result.push_back(it.str()); + } + if (result != expected) { + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result)) + << td::tag("expected", td::format::as_array(expected)); + } +} + +TEST(MessageEntities, tg_url) { + check_tg_url("", {}); + check_tg_url("tg://", {}); + check_tg_url("tg://a", {"tg://a"}); + check_tg_url("a", {}); + check_tg_url("stg://a", {"tg://a"}); + check_tg_url("asd asdas das ton:asd tg:test ton://resolve tg://resolve TON://_-RESOLVE_- TG://-_RESOLVE-_", + {"ton://resolve", "tg://resolve", "TON://_-RESOLVE_-", "TG://-_RESOLVE-_"}); + check_tg_url("tg:test/", {}); + check_tg_url("tg:/test/", {}); + check_tg_url("tg://test/", {"tg://test/"}); + check_tg_url("tg://test/?", {"tg://test/"}); + check_tg_url("tg://test/#", {"tg://test/#"}); + check_tg_url("tg://test?", {"tg://test"}); + check_tg_url("tg://test#", {"tg://test"}); + check_tg_url("tg://test/―asd―?asd=asd&asdas=―#――――", {"tg://test/―asd―?asd=asd&asdas=―#――――"}); + check_tg_url("tg://test/?asd", {"tg://test/?asd"}); + check_tg_url("tg://test/?.:;,('?!`.:;,('?!`", {"tg://test/"}); + check_tg_url("tg://test/#asdf", {"tg://test/#asdf"}); + check_tg_url("tg://test?asdf", {"tg://test?asdf"}); + check_tg_url("tg://test#asdf", {"tg://test#asdf"}); + check_tg_url("tg://test?as‖df", {"tg://test?as"}); + check_tg_url("tg://test?sadf", {"tg://test?as"}); + check_tg_url("tg://test?as\"df", {"tg://test?as"}); + check_tg_url("tg://test?as«df", {"tg://test?as"}); + check_tg_url("tg://test?as»df", {"tg://test?as"}); + check_tg_url("tg://test?as(df", {"tg://test?as(df"}); + check_tg_url("tg://test?as)df", {"tg://test?as)df"}); + check_tg_url("tg://test?as[df", {"tg://test?as[df"}); + check_tg_url("tg://test?as]df", {"tg://test?as]df"}); + check_tg_url("tg://test?as{df", {"tg://test?as{df"}); + check_tg_url("tg://test?as'df", {"tg://test?as'df"}); + check_tg_url("tg://test?as}df", {"tg://test?as}df"}); + check_tg_url("tg://test?as$df", {"tg://test?as$df"}); + check_tg_url("tg://test?as%df", {"tg://test?as%df"}); + check_tg_url("tg://%30/sccct", {}); + check_tg_url("tg://test:asd@google.com:80", {"tg://test"}); + check_tg_url("tg://google.com", {"tg://google"}); + check_tg_url("tg://google/.com", {"tg://google/.com"}); + check_tg_url("tg://127.0.0.1", {"tg://127"}); + check_tg_url("tg://б.а.н.а.на", {}); +} + +static void check_is_email_address(const td::string &str, bool expected) { + bool result = td::is_email_address(str); LOG_IF(FATAL, result != expected) << "Expected " << expected << " as result of is_email_address(" << str << ")"; } @@ -183,79 +355,79 @@ TEST(MessageEntities, is_email_address) { check_is_email_address("a.ab", false); check_is_email_address("a.bc@d.ef", true); - vector bad_userdatas = {"", - "a.a.a.a.a.a.a.a.a.a.a.a", - "+.+.+.+.+.+", - "*.a.a", - "a.*.a", - "a.a.*", - "a.a.", - "a.a.abcdefghijklmnopqrstuvwxyz0123456789", - "a.abcdefghijklmnopqrstuvwxyz0.a", - "abcdefghijklmnopqrstuvwxyz0.a.a"}; - vector good_userdatas = {"a.a.a.a.a.a.a.a.a.a.a", - "a+a+a+a+a+a+a+a+a+a+a", - "+.+.+.+.+._", - "aozAQZ0-5-9_+-aozAQZ0-5-9_.aozAQZ0-5-9_.-._.+-", - "a.a.a", - "a.a.abcdefghijklmnopqrstuvwxyz012345678", - "a.abcdefghijklmnopqrstuvwxyz.a", - "a..a", - "abcdefghijklmnopqrstuvwxyz.a.a", - ".a.a"}; - - vector bad_domains = {"", - ".", - "abc", - "localhost", - "a.a.a.a.a.a.a.ab", - ".......", - "a.a.a.a.a.a+ab", - "a+a.a.a.a.a.ab", - "a.a.a.a.a.a.a", - "a.a.a.a.a.a.abcdefg", - "a.a.a.a.a.a.ab0yz", - "a.a.a.a.a.a.ab9yz", - "a.a.a.a.a.a.ab-yz", - "a.a.a.a.a.a.ab_yz", - "a.a.a.a.a.a.ab*yz", - ".ab", - ".a.ab", - "a..ab", - "a.a.a..a.ab", - ".a.a.a.a.ab", - "abcdefghijklmnopqrstuvwxyz01234.ab", - "ab0cd.abd.aA*sd.0.9.0-9.ABOYZ", - "ab*cd.abd.aAasd.0.9.0-9.ABOYZ", - "ab0cd.abd.aAasd.0.9.0*9.ABOYZ", - "*b0cd.ab_d.aA-sd.0.9.0-9.ABOYZ", - "ab0c*.ab_d.aA-sd.0.9.0-9.ABOYZ", - "ab0cd.ab_d.aA-sd.0.9.0-*.ABOYZ", - "ab0cd.ab_d.aA-sd.0.9.*-9.ABOYZ", - "-b0cd.ab_d.aA-sd.0.9.0-9.ABOYZ", - "ab0c-.ab_d.aA-sd.0.9.0-9.ABOYZ", - "ab0cd.ab_d.aA-sd.-.9.0-9.ABOYZ", - "ab0cd.ab_d.aA-sd.0.9.--9.ABOYZ", - "ab0cd.ab_d.aA-sd.0.9.0--.ABOYZ", - "_b0cd.ab_d.aA-sd.0.9.0-9.ABOYZ", - "ab0c_.ab_d.aA-sd.0.9.0-9.ABOYZ", - "ab0cd.ab_d.aA-sd._.9.0-9.ABOYZ", - "ab0cd.ab_d.aA-sd.0.9._-9.ABOYZ", - "ab0cd.ab_d.aA-sd.0.9.0-_.ABOYZ", - "-.ab_d.aA-sd.0.9.0-9.ABOYZ", - "ab0cd.ab_d.-.0.9.0-9.ABOYZ", - "ab0cd.ab_d.aA-sd.0.9.-.ABOYZ", - "_.ab_d.aA-sd.0.9.0-9.ABOYZ", - "ab0cd.ab_d._.0.9.0-9.ABOYZ", - "ab0cd.ab_d.aA-sd.0.9._.ABOYZ"}; - vector good_domains = {"a.a.a.a.a.a.ab", - "a.a.a.a.a.a.abcdef", - "a.a.a.a.a.a.aboyz", - "a.a.a.a.a.a.ABOYZ", - "a.a.a.a.a.a.AbOyZ", - "abcdefghijklmnopqrstuvwxyz0123.ab", - "ab0cd.ab_d.aA-sd.0.9.0-9.ABOYZ", - "A.Z.aA-sd.a.z.0-9.ABOYZ"}; + td::vector bad_userdatas = {"", + "a.a.a.a.a.a.a.a.a.a.a.a", + "+.+.+.+.+.+", + "*.a.a", + "a.*.a", + "a.a.*", + "a.a.", + "a.a.abcdefghijklmnopqrstuvwxyz0123456789", + "a.abcdefghijklmnopqrstuvwxyz0.a", + "abcdefghijklmnopqrstuvwxyz0.a.a"}; + td::vector good_userdatas = {"a.a.a.a.a.a.a.a.a.a.a", + "a+a+a+a+a+a+a+a+a+a+a", + "+.+.+.+.+._", + "aozAQZ0-5-9_+-aozAQZ0-5-9_.aozAQZ0-5-9_.-._.+-", + "a.a.a", + "a.a.abcdefghijklmnopqrstuvwxyz012345678", + "a.abcdefghijklmnopqrstuvwxyz.a", + "a..a", + "abcdefghijklmnopqrstuvwxyz.a.a", + ".a.a"}; + + td::vector bad_domains = {"", + ".", + "abc", + "localhost", + "a.a.a.a.a.a.a.ab", + ".......", + "a.a.a.a.a.a+ab", + "a+a.a.a.a.a.ab", + "a.a.a.a.a.a.a", + "a.a.a.a.a.a.abcdefghi", + "a.a.a.a.a.a.ab0yz", + "a.a.a.a.a.a.ab9yz", + "a.a.a.a.a.a.ab-yz", + "a.a.a.a.a.a.ab_yz", + "a.a.a.a.a.a.ab*yz", + ".ab", + ".a.ab", + "a..ab", + "a.a.a..a.ab", + ".a.a.a.a.ab", + "abcdefghijklmnopqrstuvwxyz01234.ab", + "ab0cd.abd.aA*sd.0.9.0-9.ABOYZ", + "ab*cd.abd.aAasd.0.9.0-9.ABOYZ", + "ab0cd.abd.aAasd.0.9.0*9.ABOYZ", + "*b0cd.ab_d.aA-sd.0.9.0-9.ABOYZ", + "ab0c*.ab_d.aA-sd.0.9.0-9.ABOYZ", + "ab0cd.ab_d.aA-sd.0.9.0-*.ABOYZ", + "ab0cd.ab_d.aA-sd.0.9.*-9.ABOYZ", + "-b0cd.ab_d.aA-sd.0.9.0-9.ABOYZ", + "ab0c-.ab_d.aA-sd.0.9.0-9.ABOYZ", + "ab0cd.ab_d.aA-sd.-.9.0-9.ABOYZ", + "ab0cd.ab_d.aA-sd.0.9.--9.ABOYZ", + "ab0cd.ab_d.aA-sd.0.9.0--.ABOYZ", + "_b0cd.ab_d.aA-sd.0.9.0-9.ABOYZ", + "ab0c_.ab_d.aA-sd.0.9.0-9.ABOYZ", + "ab0cd.ab_d.aA-sd._.9.0-9.ABOYZ", + "ab0cd.ab_d.aA-sd.0.9._-9.ABOYZ", + "ab0cd.ab_d.aA-sd.0.9.0-_.ABOYZ", + "-.ab_d.aA-sd.0.9.0-9.ABOYZ", + "ab0cd.ab_d.-.0.9.0-9.ABOYZ", + "ab0cd.ab_d.aA-sd.0.9.-.ABOYZ", + "_.ab_d.aA-sd.0.9.0-9.ABOYZ", + "ab0cd.ab_d._.0.9.0-9.ABOYZ", + "ab0cd.ab_d.aA-sd.0.9._.ABOYZ"}; + td::vector good_domains = {"a.a.a.a.a.a.ab", + "a.a.a.a.a.a.abcdef", + "a.a.a.a.a.a.aboyz", + "a.a.a.a.a.a.ABOYZ", + "a.a.a.a.a.a.AbOyZ", + "abcdefghijklmnopqrstuvwxyz0123.ab", + "ab0cd.ab_d.aA-sd.0.9.0-9.ABOYZ", + "A.Z.aA-sd.a.z.0-9.ABOYZ"}; for (auto &userdata : bad_userdatas) { for (auto &domain : bad_domains) { @@ -279,11 +451,11 @@ TEST(MessageEntities, is_email_address) { } } -static void check_url(string str, std::vector expected_urls, - std::vector expected_email_addresses = {}) { - auto result_slice = find_urls(str); - std::vector result_urls; - std::vector result_email_addresses; +static void check_url(const td::string &str, const td::vector &expected_urls, + td::vector expected_email_addresses = {}) { + auto result_slice = td::find_urls(str); + td::vector result_urls; + td::vector result_email_addresses; for (auto &it : result_slice) { if (!it.second) { result_urls.push_back(it.first.str()); @@ -292,12 +464,12 @@ static void check_url(string str, std::vector expected_urls, } } if (result_urls != expected_urls) { - LOG(FATAL) << tag("text", str) << tag("got", format::as_array(result_urls)) - << tag("expected", format::as_array(expected_urls)); + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result_urls)) + << td::tag("expected", td::format::as_array(expected_urls)); } if (result_email_addresses != expected_email_addresses) { - LOG(FATAL) << tag("text", str) << tag("got", format::as_array(result_email_addresses)) - << tag("expected", format::as_array(expected_email_addresses)); + LOG(FATAL) << td::tag("text", str) << td::tag("got", td::format::as_array(result_email_addresses)) + << td::tag("expected", td::format::as_array(expected_email_addresses)); } } @@ -320,7 +492,7 @@ TEST(MessageEntities, url) { check_url(".", {}); check_url("http://@google.com", {}); check_url("http://@goog.com", {}); // TODO: server fix - check_url("http://@@google.com", {"http://@@google.com"}); + check_url("http://@@google.com", {}); check_url("http://a@google.com", {"http://a@google.com"}); check_url("http://test@google.com", {"http://test@google.com"}); check_url("google.com:᪉᪉᪉᪉᪉", {"google.com"}); @@ -328,7 +500,7 @@ TEST(MessageEntities, url) { check_url("http://telegram.org", {"http://telegram.org"}); check_url("ftp://telegram.org", {"ftp://telegram.org"}); check_url("ftps://telegram.org", {}); - check_url("sftp://telegram.org", {"sftp://telegram.org"}); + check_url("sftp://telegram.org", {}); check_url("hTtPs://telegram.org", {"hTtPs://telegram.org"}); check_url("HTTP://telegram.org", {"HTTP://telegram.org"}); check_url("аHTTP://telegram.org", {"HTTP://telegram.org"}); @@ -360,7 +532,7 @@ TEST(MessageEntities, url) { check_url("http://a.0", {}); check_url("http://a.a", {}); check_url("google.com:1#ab c", {"google.com:1#ab"}); - check_url("google.com:1#", {"google.com:1#"}); + check_url("google.com:1#", {"google.com:1"}); check_url("google.com:1#1", {"google.com:1#1"}); check_url("google.com:00000001/abs", {"google.com:00000001/abs"}); check_url("google.com:000000065535/abs", {"google.com:000000065535/abs"}); @@ -391,7 +563,8 @@ TEST(MessageEntities, url) { check_url("http://ÀТеСт.МоСкВач", {"http://ÀТеСт.МоСкВач"}); check_url("ÀÁ.com. ÀÁ.com.", {"ÀÁ.com", "ÀÁ.com"}); check_url("ÀÁ.com,ÀÁ.com.", {"ÀÁ.com", "ÀÁ.com"}); - check_url("teiegram.org", {}); + check_url("teiegram.org/test", {}); + check_url("TeiegraM.org/test", {}); check_url("http://test.google.com/?q=abc()}[]def", {"http://test.google.com/?q=abc()"}); check_url("http://test.google.com/?q=abc([{)]}def", {"http://test.google.com/?q=abc([{)]}def"}); check_url("http://test.google.com/?q=abc(){}]def", {"http://test.google.com/?q=abc(){}"}); @@ -402,18 +575,20 @@ TEST(MessageEntities, url) { check_url("http://google_.com", {}); check_url("http://google._com_", {}); check_url("http://[2001:4860:0:2001::68]/", {}); // TODO + check_url("tg://resolve", {}); check_url("test.abd", {}); check_url("/.b/..a @.....@/. a.ba", {"a.ba"}); check_url("bbbbbbbbbbbbbb.@.@", {}); check_url("http://google.com/", {"http://google.com/"}); check_url("http://google.com?", {"http://google.com"}); - check_url("http://google.com#", {"http://google.com#"}); + check_url("http://google.com#", {"http://google.com"}); + check_url("http://google.com##", {"http://google.com##"}); check_url("http://google.com/?", {"http://google.com/"}); check_url("https://www.google.com/ab,", {"https://www.google.com/ab"}); check_url("@.", {}); check_url( "a.b.google.com dfsknnfs gsdfgsg http://códuia.de/ dffdg,\" 12)(cpia.de/())(\" http://гришка.рф/ sdufhdf " - "http://xn--80afpi2a3c.xn--p1ai/ I have a good time.Thanks, guys!\n\n(hdfughidufhgdis)go#ogle.com гришка.рф " + "http://xn--80afpi2a3c.xn--p1ai/ I have a good time.Thanks, guys!\n\n(hdfughidufhgdis) go#ogle.com гришка.рф " "hsighsdf gi почта.рф\n\n✪df.ws/123 " "xn--80afpi2a3c.xn--p1ai\n\nhttp://foo.com/blah_blah\nhttp://foo.com/blah_blah/\n(Something like " "http://foo.com/blah_blah)\nhttp://foo.com/blah_blah_(wikipedi8989a_Вася)\n(Something like " @@ -469,7 +644,7 @@ TEST(MessageEntities, url) { "google.com/", "google.com/#", "google.com", - "google.com#"}); + "google.com"}); check_url("https://t.…", {}); check_url("('http://telegram.org/a-b/?br=ie&lang=en',)", {"http://telegram.org/a-b/?br=ie&lang=en"}); check_url("https://ai.telegram.org/bot%20bot/test-...", {"https://ai.telegram.org/bot%20bot/test-"}); @@ -482,11 +657,11 @@ TEST(MessageEntities, url) { check_url("https://a.de}bc@c.com", {"https://a.de"}, {"bc@c.com"}); check_url("https://a.de(bc@c.com", {"https://a.de"}, {"bc@c.com"}); check_url("https://a.de)bc@c.com", {"https://a.de"}, {"bc@c.com"}); - check_url("https://a.de\\bc@c.com", {"https://a.de\\bc@c.com"}); + check_url("https://a.debc@c.com", {"https://a.debc@c.com"}); check_url("https://a.de'bc@c.com", {"https://a.de"}, {"bc@c.com"}); check_url("https://a.de`bc@c.com", {"https://a.de"}, {"bc@c.com"}); - check_url("https://a.bc:de.fg@c.com", {"https://a.bc:de.fg@c.com"}); - check_url("https://a:h.bc:de.fg@c.com", {"https://a:h.bc:de.fg@c.com"}); + check_url("https://a.bcde.fg@c.com", {"https://a.bcde.fg@c.com"}); + check_url("https://a:h.bcde.fg@c.com", {"https://a:h.bcde.fg@c.com"}); check_url("https://abc@c.com", {"https://abc@c.com"}); check_url("https://de[bc@c.com", {}, {"bc@c.com"}); check_url("https://de/bc@c.com", {}); @@ -515,8 +690,9 @@ TEST(MessageEntities, url) { check_url("", {"http://www.ics.uci.edu/pub/ietf/uri/historical.html#WARNING"}); check_url("Look :test@example.com", {":test@example.com"}, {}); // TODO {}, {"test@example.com"} + check_url("Look mailto:test@example.com", {}, {"test@example.com"}); check_url("http://test.com#a", {"http://test.com#a"}); - check_url("http://test.com#", {"http://test.com#"}); + check_url("http://test.com#", {"http://test.com"}); check_url("http://test.com?#", {"http://test.com?#"}); check_url("http://test.com/?#", {"http://test.com/?#"}); check_url("https://t.me/abcdef…", {"https://t.me/abcdef"}); @@ -526,4 +702,1210 @@ TEST(MessageEntities, url) { check_url("https://t…", {}); check_url("👉http://ab.com/cdefgh-1IJ", {"http://ab.com/cdefgh-1IJ"}); check_url("...👉http://ab.com/cdefgh-1IJ", {}); // TODO + check_url(".?", {}); + check_url("http://test―‑@―google―.―com―/―–―‐―/―/―/―?―‑―#―――", {"http://test―‑@―google―.―com―/―–―‐―/―/―/―?―‑―#―――"}); + check_url("http://google.com/‖", {"http://google.com/"}); + check_url("a@b@c.com", {}, {}); + check_url("abc@c.com@d.com", {}); + check_url("a@b.com:c@1", {}, {"a@b.com"}); + check_url("test@test.software", {}, {"test@test.software"}); + check_url("a:b?@gmail.com", {}); + check_url("a?:b@gmail.com", {}); + check_url("a#:b@gmail.com", {}); + check_url("a:b#@gmail.com", {}); + check_url("a!:b@gmail.com", {"a!:b@gmail.com"}); + check_url("a:b!@gmail.com", {"a:b!@gmail.com"}); + check_url("http://test_.com", {}); + check_url("test_.com", {}); + check_url("_test.com", {}); + check_url("_.test.com", {"_.test.com"}); +} + +static void check_fix_formatted_text(td::string str, td::vector entities, + const td::string &expected_str, + const td::vector &expected_entities, bool allow_empty = true, + bool skip_new_entities = false, bool skip_bot_commands = false, + bool for_draft = true) { + ASSERT_TRUE(td::fix_formatted_text(str, entities, allow_empty, skip_new_entities, skip_bot_commands, true, for_draft) + .is_ok()); + ASSERT_STREQ(expected_str, str); + ASSERT_EQ(expected_entities, entities); +} + +static void check_fix_formatted_text(td::string str, td::vector entities, bool allow_empty, + bool skip_new_entities, bool skip_bot_commands, bool for_draft) { + ASSERT_TRUE(td::fix_formatted_text(str, entities, allow_empty, skip_new_entities, skip_bot_commands, true, for_draft) + .is_error()); +} + +TEST(MessageEntities, fix_formatted_text) { + td::string str; + td::string fixed_str; + for (auto i = 0; i <= 32; i++) { + str += static_cast(i); + if (i != 13) { + if (i != 10) { + fixed_str += ' '; + } else { + fixed_str += str.back(); + } + } + } + + check_fix_formatted_text(str, {}, "", {}, true, true, true, true); + check_fix_formatted_text(str, {}, "", {}, true, true, false, true); + check_fix_formatted_text(str, {}, "", {}, true, false, true, true); + check_fix_formatted_text(str, {}, "", {}, true, false, false, true); + check_fix_formatted_text(str, {}, "", {}, true, false, false, false); + check_fix_formatted_text(str, {}, false, false, false, false); + check_fix_formatted_text(str, {}, false, false, false, true); + + check_fix_formatted_text(" aba\n ", {}, " aba\n ", {}, true, true, true, true); + check_fix_formatted_text(" aba\n ", {}, "aba", {}, true, true, true, false); + check_fix_formatted_text(" \n ", {}, "", {}, true, true, true, true); + check_fix_formatted_text(" \n ", {}, "", {}, true, true, true, false); + check_fix_formatted_text(" \n ", {}, false, true, true, false); + + str += "a \r\n "; + fixed_str += "a \n "; + + for (td::int32 i = 33; i <= 35; i++) { + td::vector entities; + entities.emplace_back(td::MessageEntity::Type::Pre, 0, i); + + td::vector fixed_entities = entities; + fixed_entities.back().length = i - 1; + check_fix_formatted_text(str, entities, fixed_str, fixed_entities, true, false, false, true); + + td::string expected_str = fixed_str.substr(0, 33); + fixed_entities.back().length = i == 33 ? 32 : 33; + check_fix_formatted_text(str, entities, expected_str, fixed_entities, false, false, false, false); + } + + for (td::int32 i = 33; i <= 35; i++) { + td::vector entities; + entities.emplace_back(td::MessageEntity::Type::Bold, 0, i); + + td::vector fixed_entities; + if (i != 33) { + fixed_entities.emplace_back(td::MessageEntity::Type::Bold, 32, i - 33); + } + check_fix_formatted_text(str, entities, fixed_str, fixed_entities, true, false, false, true); + + td::string expected_str; + if (i != 33) { + fixed_entities.back().offset = 0; + fixed_entities.back().length = 1; + } + expected_str = "a"; + check_fix_formatted_text(str, entities, expected_str, fixed_entities, false, false, false, false); + } + + str = "👉 👉 "; + for (int i = 0; i < 10; i++) { + td::vector entities; + entities.emplace_back(td::MessageEntity::Type::Bold, i, 1); + if (i != 2 && i != 5 && i != 6) { + check_fix_formatted_text(str, entities, true, true, true, true); + check_fix_formatted_text(str, entities, false, false, false, false); + } else { + check_fix_formatted_text(str, entities, str, {}, true, true, true, true); + check_fix_formatted_text(str, entities, str.substr(0, str.size() - 2), {}, false, false, false, false); + } + } + + str = " /test @abaca #ORD $ABC telegram.org "; + for (auto for_draft : {false, true}) { + td::int32 shift = for_draft ? 2 : 0; + td::string expected_str = for_draft ? str : str.substr(2, str.size() - 3); + + for (auto skip_new_entities : {false, true}) { + for (auto skip_bot_commands : {false, true}) { + td::vector entities; + if (!skip_new_entities) { + if (!skip_bot_commands) { + entities.emplace_back(td::MessageEntity::Type::BotCommand, shift, 5); + } + entities.emplace_back(td::MessageEntity::Type::Mention, shift + 6, 6); + entities.emplace_back(td::MessageEntity::Type::Hashtag, shift + 13, 4); + entities.emplace_back(td::MessageEntity::Type::Cashtag, shift + 18, 4); + entities.emplace_back(td::MessageEntity::Type::Url, shift + 24, 12); + } + + check_fix_formatted_text(str, {}, expected_str, entities, true, skip_new_entities, skip_bot_commands, + for_draft); + check_fix_formatted_text(str, {}, expected_str, entities, false, skip_new_entities, skip_bot_commands, + for_draft); + } + } + } + + str = "aba \r\n caba "; + td::UserId user_id(static_cast(1)); + for (td::int32 length = 1; length <= 3; length++) { + for (td::int32 offset = 0; static_cast(offset + length) <= str.size(); offset++) { + for (auto type : {td::MessageEntity::Type::Bold, td::MessageEntity::Type::Url, td::MessageEntity::Type::TextUrl, + td::MessageEntity::Type::MentionName}) { + for (auto for_draft : {false, true}) { + fixed_str = for_draft ? "aba \n caba " : "aba \n caba"; + auto fixed_length = offset <= 4 && offset + length >= 5 ? length - 1 : length; + auto fixed_offset = offset >= 5 ? offset - 1 : offset; + if (static_cast(fixed_offset) >= fixed_str.size()) { + fixed_length = 0; + } + while (static_cast(fixed_offset + fixed_length) > fixed_str.size()) { + fixed_length--; + } + if (type == td::MessageEntity::Type::Bold || type == td::MessageEntity::Type::Url) { + while (fixed_length > 0 && (fixed_str[fixed_offset] == ' ' || fixed_str[fixed_offset] == '\n')) { + fixed_offset++; + fixed_length--; + } + } + + td::vector entities; + entities.emplace_back(type, offset, length); + if (type == td::MessageEntity::Type::TextUrl) { + entities.back().argument = "t.me"; + } else if (type == td::MessageEntity::Type::MentionName) { + entities.back().user_id = user_id; + } + td::vector fixed_entities; + if (fixed_length > 0) { + for (auto i = 0; i < length; i++) { + if (!td::is_space(str[offset + i]) || type == td::MessageEntity::Type::TextUrl || + type == td::MessageEntity::Type::MentionName) { + fixed_entities.emplace_back(type, fixed_offset, fixed_length); + if (type == td::MessageEntity::Type::TextUrl) { + fixed_entities.back().argument = "t.me"; + } else if (type == td::MessageEntity::Type::MentionName) { + fixed_entities.back().user_id = user_id; + } + break; + } + } + } + check_fix_formatted_text(str, entities, fixed_str, fixed_entities, true, false, false, for_draft); + } + } + } + } + + str = "aba caba"; + for (td::int32 length = -10; length <= 10; length++) { + for (td::int32 offset = -10; offset <= 10; offset++) { + td::vector entities; + entities.emplace_back(td::MessageEntity::Type::Bold, offset, length); + if (length < 0 || offset < 0 || (length > 0 && static_cast(length + offset) > str.size())) { + check_fix_formatted_text(str, entities, true, false, false, false); + check_fix_formatted_text(str, entities, false, false, false, true); + continue; + } + + td::vector fixed_entities; + if (length > 0) { + if (offset == 3) { + if (length >= 2) { + fixed_entities.emplace_back(td::MessageEntity::Type::Bold, offset + 1, length - 1); + } + } else { + fixed_entities.emplace_back(td::MessageEntity::Type::Bold, offset, length); + } + } + + check_fix_formatted_text(str, entities, str, fixed_entities, true, false, false, false); + check_fix_formatted_text(str, entities, str, fixed_entities, false, false, false, true); + } + } + + str = "abadcaba"; + for (td::int32 length = 1; length <= 7; length++) { + for (td::int32 offset = 0; offset <= 8 - length; offset++) { + for (td::int32 length2 = 1; length2 <= 7; length2++) { + for (td::int32 offset2 = 0; offset2 <= 8 - length2; offset2++) { + if (offset != offset2) { + td::vector entities; + entities.emplace_back(td::MessageEntity::Type::TextUrl, offset, length, "t.me"); + entities.emplace_back(td::MessageEntity::Type::TextUrl, offset2, length2, "t.me"); + entities.emplace_back(td::MessageEntity::Type::TextUrl, offset2 + length2, 1); + td::vector fixed_entities = entities; + fixed_entities.pop_back(); + std::sort(fixed_entities.begin(), fixed_entities.end()); + if (fixed_entities[0].offset + fixed_entities[0].length > fixed_entities[1].offset) { + fixed_entities.pop_back(); + } + check_fix_formatted_text(str, entities, str, fixed_entities, false, false, false, false); + } + } + } + } + } + + for (auto text : {" \n ➡️ ➡️ ➡️ ➡️ \n ", "\n\n\nab cd ef gh "}) { + str = text; + td::vector entities; + td::vector fixed_entities; + + auto length = td::narrow_cast(td::utf8_utf16_length(str)); + for (int i = 0; i < 10; i++) { + if ((i + 1) * 3 + 2 <= length) { + entities.emplace_back(td::MessageEntity::Type::Bold, (i + 1) * 3, 2); + } + if ((i + 2) * 3 <= length) { + entities.emplace_back(td::MessageEntity::Type::Italic, (i + 1) * 3 + 2, 1); + } + + if (i < 4) { + fixed_entities.emplace_back(td::MessageEntity::Type::Bold, i * 3, 2); + } + } + + check_fix_formatted_text(str, entities, td::utf8_utf16_substr(str, 3, 11).str(), fixed_entities, false, false, + false, false); + } + + for (td::string text : {"\t", "\r", "\n", "\t ", "\r ", "\n "}) { + for (auto type : {td::MessageEntity::Type::Bold, td::MessageEntity::Type::TextUrl}) { + check_fix_formatted_text(text, {{type, 0, 1, "http://telegram.org/"}}, "", {}, true, false, false, true); + } + } + check_fix_formatted_text("\r ", {{td::MessageEntity::Type::Bold, 0, 2}, {td::MessageEntity::Type::Underline, 0, 1}}, + "", {}, true, false, false, true); + check_fix_formatted_text("a \r", {{td::MessageEntity::Type::Bold, 0, 3}, {td::MessageEntity::Type::Underline, 2, 1}}, + "a ", {{td::MessageEntity::Type::Bold, 0, 2}}, true, false, false, true); + check_fix_formatted_text("a \r ", {{td::MessageEntity::Type::Bold, 0, 4}, {td::MessageEntity::Type::Underline, 2, 1}}, + "a ", {{td::MessageEntity::Type::Bold, 0, 2}}, true, false, false, true); + check_fix_formatted_text( + "a \r b", {{td::MessageEntity::Type::Bold, 0, 5}, {td::MessageEntity::Type::Underline, 2, 1}}, "a b", + {{td::MessageEntity::Type::Bold, 0, 2}, {td::MessageEntity::Type::Bold, 3, 1}}, true, false, false, true); + + check_fix_formatted_text("a\rbc\r", + {{td::MessageEntity::Type::Italic, 0, 1}, + {td::MessageEntity::Type::Bold, 0, 2}, + {td::MessageEntity::Type::Italic, 3, 2}, + {td::MessageEntity::Type::Bold, 3, 1}}, + "abc", + {{td::MessageEntity::Type::Bold, 0, 1}, + {td::MessageEntity::Type::Italic, 0, 1}, + {td::MessageEntity::Type::Bold, 2, 1}, + {td::MessageEntity::Type::Italic, 2, 1}}); + check_fix_formatted_text("a ", {{td::MessageEntity::Type::Italic, 0, 2}, {td::MessageEntity::Type::Bold, 0, 1}}, "a", + {{td::MessageEntity::Type::Bold, 0, 1}, {td::MessageEntity::Type::Italic, 0, 1}}, false, + false, false, false); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Italic, 1, 1}, {td::MessageEntity::Type::Italic, 0, 1}}, + "abc", {{td::MessageEntity::Type::Italic, 0, 2}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Italic, 1, 1}, {td::MessageEntity::Type::Italic, 1, 1}}, + "abc", {{td::MessageEntity::Type::Italic, 1, 1}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Italic, 0, 2}, {td::MessageEntity::Type::Italic, 1, 2}}, + "abc", {{td::MessageEntity::Type::Italic, 0, 3}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Italic, 0, 2}, {td::MessageEntity::Type::Italic, 2, 1}}, + "abc", {{td::MessageEntity::Type::Italic, 0, 3}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Italic, 0, 1}, {td::MessageEntity::Type::Italic, 2, 1}}, + "abc", {{td::MessageEntity::Type::Italic, 0, 1}, {td::MessageEntity::Type::Italic, 2, 1}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Italic, 0, 2}, {td::MessageEntity::Type::Bold, 1, 2}}, + "abc", + {{td::MessageEntity::Type::Italic, 0, 1}, + {td::MessageEntity::Type::Bold, 1, 2}, + {td::MessageEntity::Type::Italic, 1, 1}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Italic, 0, 2}, {td::MessageEntity::Type::Bold, 2, 1}}, + "abc", {{td::MessageEntity::Type::Italic, 0, 2}, {td::MessageEntity::Type::Bold, 2, 1}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Italic, 0, 1}, {td::MessageEntity::Type::Bold, 2, 1}}, + "abc", {{td::MessageEntity::Type::Italic, 0, 1}, {td::MessageEntity::Type::Bold, 2, 1}}); + check_fix_formatted_text("@tests @tests", {{td::MessageEntity::Type::Italic, 0, 13}}, "@tests @tests", + {{td::MessageEntity::Type::Mention, 0, 6}, + {td::MessageEntity::Type::Italic, 0, 6}, + {td::MessageEntity::Type::Mention, 7, 6}, + {td::MessageEntity::Type::Italic, 7, 6}}); + + // __a~b~__ + check_fix_formatted_text( + "ab", {{td::MessageEntity::Type::Underline, 0, 2}, {td::MessageEntity::Type::Strikethrough, 1, 1}}, "ab", + {{td::MessageEntity::Type::Underline, 0, 1}, + {td::MessageEntity::Type::Underline, 1, 1}, + {td::MessageEntity::Type::Strikethrough, 1, 1}}); + check_fix_formatted_text("ab", + {{td::MessageEntity::Type::Underline, 0, 1}, + {td::MessageEntity::Type::Underline, 1, 1}, + {td::MessageEntity::Type::Strikethrough, 1, 1}}, + "ab", + {{td::MessageEntity::Type::Underline, 0, 1}, + {td::MessageEntity::Type::Underline, 1, 1}, + {td::MessageEntity::Type::Strikethrough, 1, 1}}); + check_fix_formatted_text( + "ab", {{td::MessageEntity::Type::Strikethrough, 0, 2}, {td::MessageEntity::Type::Underline, 1, 1}}, "ab", + {{td::MessageEntity::Type::Strikethrough, 0, 1}, + {td::MessageEntity::Type::Underline, 1, 1}, + {td::MessageEntity::Type::Strikethrough, 1, 1}}); + check_fix_formatted_text("ab", + {{td::MessageEntity::Type::Strikethrough, 0, 1}, + {td::MessageEntity::Type::Strikethrough, 1, 1}, + {td::MessageEntity::Type::Underline, 1, 1}}, + "ab", + {{td::MessageEntity::Type::Strikethrough, 0, 1}, + {td::MessageEntity::Type::Underline, 1, 1}, + {td::MessageEntity::Type::Strikethrough, 1, 1}}); + + // __||a||b__ + check_fix_formatted_text("ab", {{td::MessageEntity::Type::Underline, 0, 2}, {td::MessageEntity::Type::Spoiler, 0, 1}}, + "ab", + {{td::MessageEntity::Type::Underline, 0, 2}, {td::MessageEntity::Type::Spoiler, 0, 1}}); + check_fix_formatted_text("ab", + {{td::MessageEntity::Type::Underline, 0, 1}, + {td::MessageEntity::Type::Underline, 1, 1}, + {td::MessageEntity::Type::Spoiler, 0, 1}}, + "ab", + {{td::MessageEntity::Type::Underline, 0, 2}, {td::MessageEntity::Type::Spoiler, 0, 1}}); + + // _*a*_\r_*b*_ + check_fix_formatted_text("a\rb", + {{td::MessageEntity::Type::Bold, 0, 1}, + {td::MessageEntity::Type::Italic, 0, 1}, + {td::MessageEntity::Type::Bold, 2, 1}, + {td::MessageEntity::Type::Italic, 2, 1}}, + "ab", {{td::MessageEntity::Type::Bold, 0, 2}, {td::MessageEntity::Type::Italic, 0, 2}}); + check_fix_formatted_text("a\nb", + {{td::MessageEntity::Type::Bold, 0, 1}, + {td::MessageEntity::Type::Italic, 0, 1}, + {td::MessageEntity::Type::Bold, 2, 1}, + {td::MessageEntity::Type::Italic, 2, 1}}, + "a\nb", + {{td::MessageEntity::Type::Bold, 0, 1}, + {td::MessageEntity::Type::Italic, 0, 1}, + {td::MessageEntity::Type::Bold, 2, 1}, + {td::MessageEntity::Type::Italic, 2, 1}}); + + // ||`a`|| + check_fix_formatted_text("a", {{td::MessageEntity::Type::Pre, 0, 1}, {td::MessageEntity::Type::Spoiler, 0, 1}}, "a", + {{td::MessageEntity::Type::Pre, 0, 1}}); + check_fix_formatted_text("a", {{td::MessageEntity::Type::Spoiler, 0, 1}, {td::MessageEntity::Type::Pre, 0, 1}}, "a", + {{td::MessageEntity::Type::Pre, 0, 1}}); + + check_fix_formatted_text("abc", + {{td::MessageEntity::Type::Pre, 0, 3}, {td::MessageEntity::Type::Strikethrough, 1, 1}}, + "abc", {{td::MessageEntity::Type::Pre, 0, 3}}); + check_fix_formatted_text( + "abc", {{td::MessageEntity::Type::Pre, 1, 1}, {td::MessageEntity::Type::Strikethrough, 0, 3}}, "abc", + {{td::MessageEntity::Type::Strikethrough, 0, 1}, + {td::MessageEntity::Type::Pre, 1, 1}, + {td::MessageEntity::Type::Strikethrough, 2, 1}}); + check_fix_formatted_text( + "abc", {{td::MessageEntity::Type::Pre, 1, 1}, {td::MessageEntity::Type::Strikethrough, 1, 2}}, "abc", + {{td::MessageEntity::Type::Pre, 1, 1}, {td::MessageEntity::Type::Strikethrough, 2, 1}}); + check_fix_formatted_text( + "abc", {{td::MessageEntity::Type::Pre, 1, 1}, {td::MessageEntity::Type::Strikethrough, 0, 2}}, "abc", + {{td::MessageEntity::Type::Strikethrough, 0, 1}, {td::MessageEntity::Type::Pre, 1, 1}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::Pre, 0, 3}, {td::MessageEntity::Type::BlockQuote, 1, 1}}, + "abc", {{td::MessageEntity::Type::BlockQuote, 1, 1}}); + check_fix_formatted_text("abc", {{td::MessageEntity::Type::BlockQuote, 0, 3}, {td::MessageEntity::Type::Pre, 1, 1}}, + "abc", {{td::MessageEntity::Type::BlockQuote, 0, 3}, {td::MessageEntity::Type::Pre, 1, 1}}); + + check_fix_formatted_text("example.com", {}, "example.com", {{td::MessageEntity::Type::Url, 0, 11}}); + check_fix_formatted_text("example.com", {{td::MessageEntity::Type::Pre, 0, 3}}, "example.com", + {{td::MessageEntity::Type::Pre, 0, 3}}); + check_fix_formatted_text("example.com", {{td::MessageEntity::Type::BlockQuote, 0, 3}}, "example.com", + {{td::MessageEntity::Type::BlockQuote, 0, 3}}); + check_fix_formatted_text("example.com", {{td::MessageEntity::Type::BlockQuote, 0, 11}}, "example.com", + {{td::MessageEntity::Type::BlockQuote, 0, 11}, {td::MessageEntity::Type::Url, 0, 11}}); + check_fix_formatted_text("example.com", {{td::MessageEntity::Type::Italic, 0, 11}}, "example.com", + {{td::MessageEntity::Type::Url, 0, 11}, {td::MessageEntity::Type::Italic, 0, 11}}); + check_fix_formatted_text("example.com", {{td::MessageEntity::Type::Italic, 0, 3}}, "example.com", + {{td::MessageEntity::Type::Url, 0, 11}, {td::MessageEntity::Type::Italic, 0, 3}}); + check_fix_formatted_text("example.com a", {{td::MessageEntity::Type::Italic, 0, 13}}, "example.com a", + {{td::MessageEntity::Type::Url, 0, 11}, + {td::MessageEntity::Type::Italic, 0, 11}, + {td::MessageEntity::Type::Italic, 12, 1}}); + check_fix_formatted_text("a example.com", {{td::MessageEntity::Type::Italic, 0, 13}}, "a example.com", + {{td::MessageEntity::Type::Italic, 0, 2}, + {td::MessageEntity::Type::Url, 2, 11}, + {td::MessageEntity::Type::Italic, 2, 11}}); + + for (size_t test_n = 0; test_n < 100000; test_n++) { + bool is_url = td::Random::fast_bool(); + td::int32 url_offset = 0; + td::int32 url_end = 0; + if (is_url) { + str = td::string(td::Random::fast(1, 5), 'a') + ":example.com:" + td::string(td::Random::fast(1, 5), 'a'); + url_offset = static_cast(str.find('e')); + url_end = url_offset + 11; + } else { + str = td::string(td::Random::fast(1, 20), 'a'); + } + + auto n = td::Random::fast(1, 20); + td::vector entities; + for (int j = 0; j < n; j++) { + td::int32 type = td::Random::fast(4, 16); + td::int32 offset = td::Random::fast(0, static_cast(str.size()) - 1); + auto max_length = static_cast(str.size() - offset); + if ((test_n & 1) != 0 && max_length > 4) { + max_length = 4; + } + td::int32 length = td::Random::fast(0, max_length); + entities.emplace_back(static_cast(type), offset, length); + } + + auto get_type_mask = [](std::size_t length, const td::vector &entities) { + td::vector result(length); + for (auto &entity : entities) { + for (auto pos = 0; pos < entity.length; pos++) { + result[entity.offset + pos] |= (1 << static_cast(entity.type)); + } + } + return result; + }; + auto old_type_mask = get_type_mask(str.size(), entities); + ASSERT_TRUE(td::fix_formatted_text(str, entities, false, false, true, true, false).is_ok()); + auto new_type_mask = get_type_mask(str.size(), entities); + auto splittable_mask = (1 << 5) | (1 << 6) | (1 << 14) | (1 << 15); + auto pre_mask = (1 << 7) | (1 << 8) | (1 << 9); + for (std::size_t pos = 0; pos < str.size(); pos++) { + if ((new_type_mask[pos] & pre_mask) != 0) { + ASSERT_EQ(0, new_type_mask[pos] & splittable_mask); + } else { + ASSERT_EQ(old_type_mask[pos] & splittable_mask, new_type_mask[pos] & splittable_mask); + } + } + bool keep_url = is_url; + td::MessageEntity url_entity(td::MessageEntity::Type::Url, url_offset, url_end - url_offset); + for (auto &entity : entities) { + if (entity == url_entity) { + continue; + } + td::int32 offset = entity.offset; + td::int32 end = offset + entity.length; + + if (keep_url && ((1 << static_cast(entity.type)) & splittable_mask) == 0 && + !(end <= url_offset || url_end <= offset)) { + keep_url = (entity.type == td::MessageEntity::Type::BlockQuote && offset <= url_offset && url_end <= end); + } + } + ASSERT_EQ(keep_url, std::count(entities.begin(), entities.end(), url_entity) == 1); + + for (size_t i = 0; i < entities.size(); i++) { + auto type_mask = 1 << static_cast(entities[i].type); + for (size_t j = i + 1; j < entities.size(); j++) { + // sorted + ASSERT_TRUE(entities[j].offset > entities[i].offset || + (entities[j].offset == entities[i].offset && entities[j].length <= entities[i].length)); + + // not intersecting + ASSERT_TRUE(entities[j].offset >= entities[i].offset + entities[i].length || + entities[j].offset + entities[j].length <= entities[i].offset + entities[i].length); + + if (entities[j].offset < entities[i].offset + entities[i].length) { // if nested + // types are different + ASSERT_TRUE(entities[j].type != entities[i].type); + + // pre can't contain other entities + ASSERT_TRUE((type_mask & pre_mask) == 0); + + if ((type_mask & splittable_mask) == 0 && entities[i].type != td::MessageEntity::Type::BlockQuote) { + // continuous entities can contain only splittable entities + ASSERT_TRUE(((1 << static_cast(entities[j].type)) & splittable_mask) != 0); + } + } + } + } + } + + check_fix_formatted_text( + "\xe2\x80\x8f\xe2\x80\x8f \xe2\x80\x8e\xe2\x80\x8e\xe2\x80\x8e\xe2\x80\x8c \xe2\x80\x8f\xe2\x80\x8e " + "\xe2\x80\x8f", + {}, + "\xe2\x80\x8c\xe2\x80\x8f \xe2\x80\x8c\xe2\x80\x8c\xe2\x80\x8e\xe2\x80\x8c \xe2\x80\x8c\xe2\x80\x8e " + "\xe2\x80\x8f", + {}); +} + +static void check_parse_html(td::string text, const td::string &result, const td::vector &entities) { + auto r_entities = td::parse_html(text); + ASSERT_TRUE(r_entities.is_ok()); + ASSERT_EQ(entities, r_entities.ok()); + ASSERT_STREQ(result, text); +} + +static void check_parse_html(td::string text, td::Slice error_message) { + auto r_entities = td::parse_html(text); + ASSERT_TRUE(r_entities.is_error()); + ASSERT_EQ(400, r_entities.error().code()); + ASSERT_STREQ(error_message, r_entities.error().message()); +} + +TEST(MessageEntities, parse_html) { + td::string invalid_surrogate_pair_error_message = + "Text contains invalid Unicode characters after decoding HTML entities, check for unmatched surrogate code units"; + check_parse_html("�", invalid_surrogate_pair_error_message); + check_parse_html("�", invalid_surrogate_pair_error_message); + check_parse_html("�", invalid_surrogate_pair_error_message); + check_parse_html("🏟 🏟<", "Unsupported start tag \"abac\" at byte offset 13"); + check_parse_html("🏟 🏟<", "Unsupported start tag \"abac\" at byte offset 13"); + check_parse_html("🏟 🏟<", "Empty attribute name in the tag \"i\" at byte offset 13"); + check_parse_html("🏟 🏟<", + "Expected equal sign in declaration of an attribute of the tag \"i\" at byte offset 13"); + check_parse_html("🏟 🏟<", "Unclosed start tag at byte offset 13"); + check_parse_html("🏟 🏟<", "Unclosed start tag at byte offset 13"); + check_parse_html("🏟 🏟<aa", + "Unmatched end tag at byte offset 17, expected \"\", found \"\""); + + check_parse_html("", "", {}); + check_parse_html("➡️ ➡️", "➡️ ➡️", {}); + check_parse_html("<>&"«»�", "<>&\"«»�", {}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Italic, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Italic, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Bold, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Bold, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Underline, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Underline, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Strikethrough, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Strikethrough, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Strikethrough, 5, 5}}); + check_parse_html("➡️ ➡️➡️ ➡️➡️ ➡️", "➡️ ➡️➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Italic, 5, 5}, {td::MessageEntity::Type::Bold, 10, 5}}); + check_parse_html("🏟 🏟🏟 <🏟", "🏟 🏟🏟 <🏟", {{td::MessageEntity::Type::Italic, 5, 6}}); + check_parse_html("🏟 🏟🏟 ><🏟", "🏟 🏟🏟 ><🏟", + {{td::MessageEntity::Type::Italic, 5, 7}, {td::MessageEntity::Type::Bold, 9, 3}}); + check_parse_html("🏟 🏟<a", "🏟 🏟a", "🏟 🏟a", "🏟 🏟a", "🏟 🏟a", "🏟 🏟🏟 🏟<", "🏟 🏟<🏟 🏟<", + {{td::MessageEntity::Type::Italic, 6, 6}}); + check_parse_html("🏟 🏟<a", "🏟 🏟a", "🏟 🏟", "🏟 🏟<", {}); + check_parse_html("\t", "\t", {{td::MessageEntity::Type::Italic, 0, 1}}); + check_parse_html("\r", "\r", {{td::MessageEntity::Type::Italic, 0, 1}}); + check_parse_html("\n", "\n", {{td::MessageEntity::Type::Italic, 0, 1}}); + check_parse_html("➡️ ➡️➡️ ➡️➡️ ➡️", + "➡️ ➡️➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Spoiler, 5, 5}, {td::MessageEntity::Type::Bold, 10, 5}}); + check_parse_html("🏟 🏟🏟 <🏟", "🏟 🏟🏟 <🏟", + {{td::MessageEntity::Type::Spoiler, 5, 6}}); + check_parse_html("🏟 🏟🏟 ><🏟", + "🏟 🏟🏟 ><🏟", + {{td::MessageEntity::Type::Spoiler, 5, 7}, {td::MessageEntity::Type::Bold, 9, 3}}); + check_parse_html("➡️ ➡️➡️ ➡️➡️ ➡️", + "➡️ ➡️➡️ ➡️➡️ ➡️", + {{td::MessageEntity::Type::Spoiler, 5, 5}, {td::MessageEntity::Type::Bold, 10, 5}}); + check_parse_html("🏟 🏟🏟 <🏟", "🏟 🏟🏟 <🏟", + {{td::MessageEntity::Type::Spoiler, 5, 6}}); + check_parse_html("🏟 🏟🏟 ><🏟", "🏟 🏟🏟 ><🏟", + {{td::MessageEntity::Type::Spoiler, 5, 7}, {td::MessageEntity::Type::Bold, 9, 3}}); + check_parse_html("\t", "\t", + {{td::MessageEntity::Type::TextUrl, 0, 1, "http://telegram.org/"}}); + check_parse_html("\r", "\r", + {{td::MessageEntity::Type::TextUrl, 0, 1, "http://telegram.org/"}}); + check_parse_html("\n", "\n", + {{td::MessageEntity::Type::TextUrl, 0, 1, "http://telegram.org/"}}); + check_parse_html(" ", " ", + {{td::MessageEntity::Type::Code, 0, 1}, + {td::MessageEntity::Type::Bold, 0, 1}, + {td::MessageEntity::Type::Italic, 0, 1}, + {td::MessageEntity::Type::Code, 1, 1}, + {td::MessageEntity::Type::Bold, 1, 1}, + {td::MessageEntity::Type::Italic, 1, 1}}); + check_parse_html(" ", " ", + {{td::MessageEntity::Type::Italic, 0, 3}, + {td::MessageEntity::Type::Bold, 0, 1}, + {td::MessageEntity::Type::Code, 2, 1}}); + check_parse_html(" ", " ", + {{td::MessageEntity::Type::TextUrl, 0, 1, "http://telegram.org/"}}); + check_parse_html(" ", " ", + {{td::MessageEntity::Type::TextUrl, 0, 1, "http://telegram.org/"}}); + check_parse_html(" ", " ", + {{td::MessageEntity::Type::TextUrl, 0, 1, "http://telegram.org/"}}); + check_parse_html(" ", " ", + {{td::MessageEntity::Type::TextUrl, 0, 1, "http://telegram.org/?<"}}); + check_parse_html(" ", " ", {}); + check_parse_html("telegram.org ", "telegram.org ", {}); + check_parse_html("telegram.org", "telegram.org", + {{td::MessageEntity::Type::TextUrl, 0, 12, "http://telegram.org/"}}); + check_parse_html("https://telegram.org/asdsa?asdasdwe#12e3we", "https://telegram.org/asdsa?asdasdwe#12e3we", + {{td::MessageEntity::Type::TextUrl, 0, 42, "https://telegram.org/asdsa?asdasdwe#12e3we"}}); + check_parse_html("🏟 🏟<
🏟 🏟<", "🏟 🏟<🏟 🏟<",
+                   {{td::MessageEntity::Type::Pre, 6, 6}});
+  check_parse_html("🏟 🏟<🏟 🏟<", "🏟 🏟<🏟 🏟<",
+                   {{td::MessageEntity::Type::Code, 6, 6}});
+  check_parse_html("🏟 🏟<
🏟 🏟<", "🏟 🏟<🏟 🏟<",
+                   {{td::MessageEntity::Type::Pre, 6, 6}, {td::MessageEntity::Type::Code, 6, 6}});
+  check_parse_html("🏟 🏟<
🏟 🏟<", "🏟 🏟<🏟 🏟<",
+                   {{td::MessageEntity::Type::Pre, 6, 6}, {td::MessageEntity::Type::Code, 6, 6}});
+  check_parse_html("🏟 🏟<
🏟 🏟<", "🏟 🏟<🏟 🏟<",
+                   {{td::MessageEntity::Type::PreCode, 6, 6, "fift"}});
+  check_parse_html("🏟 🏟<
🏟 🏟<", "🏟 🏟<🏟 🏟<",
+                   {{td::MessageEntity::Type::PreCode, 6, 6, "fift"}});
+  check_parse_html("🏟 🏟<
🏟 🏟< ", "🏟 🏟<🏟 🏟< ",
+                   {{td::MessageEntity::Type::Pre, 6, 7}, {td::MessageEntity::Type::Code, 6, 6}});
+  check_parse_html("🏟 🏟<
 🏟 🏟<", "🏟 🏟< 🏟 🏟<",
+                   {{td::MessageEntity::Type::Pre, 6, 7}, {td::MessageEntity::Type::Code, 7, 6}});
+  check_parse_html("➡️ ➡️➡️ ➡️➡️ ➡️",
+                   "➡️ ➡️➡️ ➡️➡️ ➡️",
+                   {{td::MessageEntity::Type::CustomEmoji, 5, 5, td::CustomEmojiId(static_cast(12345))},
+                    {td::MessageEntity::Type::Bold, 10, 5}});
+  check_parse_html("🏟 🏟🏟 <🏟", "🏟 🏟🏟 <🏟",
+                   {{td::MessageEntity::Type::CustomEmoji, 5, 6, td::CustomEmojiId(static_cast(54321))}});
+  check_parse_html("🏟 🏟🏟1", "🏟 🏟🏟1",
+                   {{td::MessageEntity::Type::Bold, 5, 3},
+                    {td::MessageEntity::Type::CustomEmoji, 5, 2, td::CustomEmojiId(static_cast(1))}});
+}
+
+static void check_parse_markdown(td::string text, const td::string &result,
+                                 const td::vector &entities) {
+  auto r_entities = td::parse_markdown_v2(text);
+  ASSERT_TRUE(r_entities.is_ok());
+  ASSERT_EQ(entities, r_entities.ok());
+  ASSERT_STREQ(result, text);
+}
+
+static void check_parse_markdown(td::string text, td::Slice error_message) {
+  auto r_entities = td::parse_markdown_v2(text);
+  ASSERT_TRUE(r_entities.is_error());
+  ASSERT_EQ(400, r_entities.error().code());
+  ASSERT_STREQ(error_message, r_entities.error().message());
+}
+
+TEST(MessageEntities, parse_markdown) {
+  td::Slice reserved_characters("]()>#+-=|{}.!");
+  td::Slice begin_characters("_*[~`");
+  for (char c = 1; c < 126; c++) {
+    if (begin_characters.find(c) != td::Slice::npos) {
+      continue;
+    }
+
+    td::string text(1, c);
+    if (reserved_characters.find(c) == td::Slice::npos) {
+      check_parse_markdown(text, text, {});
+    } else {
+      check_parse_markdown(
+          text, PSLICE() << "Character '" << c << "' is reserved and must be escaped with the preceding '\\'");
+
+      td::string escaped_text = "\\" + text;
+      check_parse_markdown(escaped_text, text, {});
+    }
+  }
+
+  check_parse_markdown("🏟 🏟_abacaba", "Can't find end of Italic entity at byte offset 9");
+  check_parse_markdown("🏟 🏟_abac * asd ", "Can't find end of Bold entity at byte offset 15");
+  check_parse_markdown("🏟 🏟_abac * asd _", "Can't find end of Italic entity at byte offset 21");
+  check_parse_markdown("🏟 🏟`", "Can't find end of Code entity at byte offset 9");
+  check_parse_markdown("🏟 🏟```", "Can't find end of Pre entity at byte offset 9");
+  check_parse_markdown("🏟 🏟```a", "Can't find end of Pre entity at byte offset 9");
+  check_parse_markdown("🏟 🏟```a ", "Can't find end of PreCode entity at byte offset 9");
+  check_parse_markdown("🏟 🏟__🏟 🏟_", "Can't find end of Italic entity at byte offset 20");
+  check_parse_markdown("🏟 🏟_🏟 🏟__", "Can't find end of Underline entity at byte offset 19");
+  check_parse_markdown("🏟 🏟```🏟 🏟`", "Can't find end of Code entity at byte offset 21");
+  check_parse_markdown("🏟 🏟```🏟 🏟_", "Can't find end of PreCode entity at byte offset 9");
+  check_parse_markdown("🏟 🏟```🏟 🏟\\`", "Can't find end of PreCode entity at byte offset 9");
+  check_parse_markdown("[telegram\\.org](asd\\)", "Can't find end of a URL at byte offset 16");
+  check_parse_markdown("[telegram\\.org](", "Can't find end of a URL at byte offset 16");
+  check_parse_markdown("[telegram\\.org](asd", "Can't find end of a URL at byte offset 16");
+  check_parse_markdown("🏟 🏟__🏟 _🏟___", "Can't find end of Italic entity at byte offset 23");
+  check_parse_markdown("🏟 🏟__", "Can't find end of Underline entity at byte offset 9");
+  check_parse_markdown("🏟 🏟||test\\|", "Can't find end of Spoiler entity at byte offset 9");
+  check_parse_markdown("🏟 🏟!", "Character '!' is reserved and must be escaped with the preceding '\\'");
+  check_parse_markdown("🏟 🏟![", "Can't find end of CustomEmoji entity at byte offset 9");
+  check_parse_markdown("🏟 🏟![👍", "Can't find end of CustomEmoji entity at byte offset 9");
+  check_parse_markdown("🏟 🏟![👍]", "Custom emoji entity must contain a tg://emoji URL");
+  check_parse_markdown("🏟 🏟![👍](tg://emoji?id=1234", "Can't find end of a custom emoji URL at byte offset 17");
+  check_parse_markdown("🏟 🏟![👍](t://emoji?id=1234)", "Custom emoji URL must have scheme tg");
+  check_parse_markdown("🏟 🏟![👍](tg:emojis?id=1234)", "Custom emoji URL must have host \"emoji\"");
+  check_parse_markdown("🏟 🏟![👍](tg://emoji#test)", "Custom emoji URL must have an emoji identifier");
+  check_parse_markdown("🏟 🏟![👍](tg://emoji?test=1#&id=25)", "Custom emoji URL must have an emoji identifier");
+  check_parse_markdown("🏟 🏟![👍](tg://emoji?test=1231&id=025)", "Invalid custom emoji identifier specified");
+
+  check_parse_markdown("", "", {});
+  check_parse_markdown("\\\\", "\\", {});
+  check_parse_markdown("\\\\\\", "\\\\", {});
+  check_parse_markdown("\\\\\\\\\\_\\*\\`", "\\\\_*`", {});
+  check_parse_markdown("➡️ ➡️", "➡️ ➡️", {});
+  check_parse_markdown("🏟 🏟``", "🏟 🏟", {});
+  check_parse_markdown("🏟 🏟_abac \\* asd _", "🏟 🏟abac * asd ", {{td::MessageEntity::Type::Italic, 5, 11}});
+  check_parse_markdown("🏟 \\.🏟_🏟\\. 🏟_", "🏟 .🏟🏟. 🏟", {{td::MessageEntity::Type::Italic, 6, 6}});
+  check_parse_markdown("\\\\\\a\\b\\c\\d\\e\\f\\1\\2\\3\\4\\➡️\\", "\\abcdef1234\\➡️\\", {});
+  check_parse_markdown("➡️ ➡️_➡️ ➡️_", "➡️ ➡️➡️ ➡️",
+                       {{td::MessageEntity::Type::Italic, 5, 5}});
+  check_parse_markdown("➡️ ➡️_➡️ ➡️_*➡️ ➡️*", "➡️ ➡️➡️ ➡️➡️ ➡️",
+                       {{td::MessageEntity::Type::Italic, 5, 5}, {td::MessageEntity::Type::Bold, 10, 5}});
+  check_parse_markdown("🏟 🏟_🏟 \\.🏟_", "🏟 🏟🏟 .🏟", {{td::MessageEntity::Type::Italic, 5, 6}});
+  check_parse_markdown("🏟 🏟_🏟 *🏟*_", "🏟 🏟🏟 🏟",
+                       {{td::MessageEntity::Type::Italic, 5, 5}, {td::MessageEntity::Type::Bold, 8, 2}});
+  check_parse_markdown("🏟 🏟_🏟 __🏟___", "🏟 🏟🏟 🏟",
+                       {{td::MessageEntity::Type::Italic, 5, 5}, {td::MessageEntity::Type::Underline, 8, 2}});
+  check_parse_markdown("🏟 🏟__🏟 _🏟_ __", "🏟 🏟🏟 🏟 ",
+                       {{td::MessageEntity::Type::Underline, 5, 6}, {td::MessageEntity::Type::Italic, 8, 2}});
+  check_parse_markdown("🏟 🏟__🏟 _🏟_\\___", "🏟 🏟🏟 🏟_",
+                       {{td::MessageEntity::Type::Underline, 5, 6}, {td::MessageEntity::Type::Italic, 8, 2}});
+  check_parse_markdown("🏟 🏟`🏟 🏟```", "🏟 🏟🏟 🏟", {{td::MessageEntity::Type::Code, 5, 5}});
+  check_parse_markdown("🏟 🏟```🏟 🏟```", "🏟 🏟 🏟",
+                       {{td::MessageEntity::Type::PreCode, 5, 3, "🏟"}});
+  check_parse_markdown("🏟 🏟```🏟\n🏟```", "🏟 🏟🏟",
+                       {{td::MessageEntity::Type::PreCode, 5, 2, "🏟"}});
+  check_parse_markdown("🏟 🏟```🏟\r🏟```", "🏟 🏟🏟",
+                       {{td::MessageEntity::Type::PreCode, 5, 2, "🏟"}});
+  check_parse_markdown("🏟 🏟```🏟\n\r🏟```", "🏟 🏟🏟",
+                       {{td::MessageEntity::Type::PreCode, 5, 2, "🏟"}});
+  check_parse_markdown("🏟 🏟```🏟\r\n🏟```", "🏟 🏟🏟",
+                       {{td::MessageEntity::Type::PreCode, 5, 2, "🏟"}});
+  check_parse_markdown("🏟 🏟```🏟\n\n🏟```", "🏟 🏟\n🏟",
+                       {{td::MessageEntity::Type::PreCode, 5, 3, "🏟"}});
+  check_parse_markdown("🏟 🏟```🏟\r\r🏟```", "🏟 🏟\r🏟",
+                       {{td::MessageEntity::Type::PreCode, 5, 3, "🏟"}});
+  check_parse_markdown("🏟 🏟```🏟 \\\\\\`🏟```", "🏟 🏟 \\`🏟",
+                       {{td::MessageEntity::Type::PreCode, 5, 5, "🏟"}});
+  check_parse_markdown("🏟 🏟**", "🏟 🏟", {});
+  check_parse_markdown("||test||", "test", {{td::MessageEntity::Type::Spoiler, 0, 4}});
+  check_parse_markdown("🏟 🏟``", "🏟 🏟", {});
+  check_parse_markdown("🏟 🏟``````", "🏟 🏟", {});
+  check_parse_markdown("🏟 🏟____", "🏟 🏟", {});
+  check_parse_markdown("`_* *_`__*` `*__", "_* *_ ",
+                       {{td::MessageEntity::Type::Code, 0, 5},
+                        {td::MessageEntity::Type::Code, 5, 1},
+                        {td::MessageEntity::Type::Bold, 5, 1},
+                        {td::MessageEntity::Type::Underline, 5, 1}});
+  check_parse_markdown("_* * ` `_", "   ",
+                       {{td::MessageEntity::Type::Italic, 0, 3},
+                        {td::MessageEntity::Type::Bold, 0, 1},
+                        {td::MessageEntity::Type::Code, 2, 1}});
+  check_parse_markdown("[](telegram.org)", "", {});
+  check_parse_markdown("[ ](telegram.org)", " ", {{td::MessageEntity::Type::TextUrl, 0, 1, "http://telegram.org/"}});
+  check_parse_markdown("[ ](as)", " ", {});
+  check_parse_markdown("[telegram\\.org]", "telegram.org",
+                       {{td::MessageEntity::Type::TextUrl, 0, 12, "http://telegram.org/"}});
+  check_parse_markdown("[telegram\\.org]a", "telegram.orga",
+                       {{td::MessageEntity::Type::TextUrl, 0, 12, "http://telegram.org/"}});
+  check_parse_markdown("[telegram\\.org](telegram.dog)", "telegram.org",
+                       {{td::MessageEntity::Type::TextUrl, 0, 12, "http://telegram.dog/"}});
+  check_parse_markdown("[telegram\\.org](https://telegram.dog?)", "telegram.org",
+                       {{td::MessageEntity::Type::TextUrl, 0, 12, "https://telegram.dog/?"}});
+  check_parse_markdown("[telegram\\.org](https://telegram.dog?\\\\\\()", "telegram.org",
+                       {{td::MessageEntity::Type::TextUrl, 0, 12, "https://telegram.dog/?\\("}});
+  check_parse_markdown("[telegram\\.org]()", "telegram.org", {});
+  check_parse_markdown("[telegram\\.org](asdasd)", "telegram.org", {});
+  check_parse_markdown("[telegram\\.org](tg:user?id=123456)", "telegram.org",
+                       {{0, 12, td::UserId(static_cast(123456))}});
+  check_parse_markdown("🏟 🏟![👍](TG://EMoJI/?test=1231&id=25#id=32)a", "🏟 🏟👍a",
+                       {{td::MessageEntity::Type::CustomEmoji, 5, 2, td::CustomEmojiId(static_cast(25))}});
+}
+
+static void check_parse_markdown_v3(td::string text, td::vector entities,
+                                    const td::string &result_text, const td::vector &result_entities,
+                                    bool fix = false) {
+  auto parsed_text = td::parse_markdown_v3({std::move(text), std::move(entities)});
+  if (fix) {
+    ASSERT_TRUE(td::fix_formatted_text(parsed_text.text, parsed_text.entities, true, true, true, true, true).is_ok());
+  }
+  ASSERT_STREQ(result_text, parsed_text.text);
+  ASSERT_EQ(result_entities, parsed_text.entities);
+  if (fix) {
+    auto markdown_text = td::get_markdown_v3(parsed_text);
+    ASSERT_TRUE(parsed_text == markdown_text || parsed_text == td::parse_markdown_v3(markdown_text));
+  }
+}
+
+static void check_parse_markdown_v3(td::string text, const td::string &result_text,
+                                    const td::vector &result_entities, bool fix = false) {
+  check_parse_markdown_v3(std::move(text), td::vector(), result_text, result_entities, fix);
+}
+
+TEST(MessageEntities, parse_markdown_v3) {
+  check_parse_markdown_v3("🏟````🏟``🏟`aba🏟```c🏟`aba🏟 daba🏟```c🏟`aba🏟```🏟 `🏟``🏟```",
+                          "🏟````🏟``🏟aba🏟```c🏟aba🏟 daba🏟c🏟`aba🏟🏟 `🏟``🏟```",
+                          {{td::MessageEntity::Type::Code, 12, 11}, {td::MessageEntity::Type::Pre, 35, 9}});
+  check_parse_markdown_v3(
+      "🏟````🏟``🏟`aba🏟```c🏟`aba🏟 daba🏟```c🏟`aba🏟🏟```🏟 `🏟``🏟```",
+      {{td::MessageEntity::Type::Italic, 12, 1},
+       {td::MessageEntity::Type::Italic, 44, 1},
+       {td::MessageEntity::Type::Bold, 45, 1},
+       {td::MessageEntity::Type::Bold, 49, 2}},
+      "🏟````🏟``🏟`aba🏟c🏟`aba🏟 daba🏟c🏟`aba🏟🏟🏟 `🏟``🏟",
+      {{td::MessageEntity::Type::Italic, 12, 1},
+       {td::MessageEntity::Type::Pre, 18, 16},
+       {td::MessageEntity::Type::Italic, 38, 1},
+       {td::MessageEntity::Type::Bold, 39, 1},
+       {td::MessageEntity::Type::Bold, 43, 2},
+       {td::MessageEntity::Type::Pre, 45, 10}});
+  check_parse_markdown_v3("` `", " ", {{td::MessageEntity::Type::Code, 0, 1}});
+  check_parse_markdown_v3("`\n`", "\n", {{td::MessageEntity::Type::Code, 0, 1}});
+  check_parse_markdown_v3("` `a", " a", {{td::MessageEntity::Type::Code, 0, 1}}, true);
+  check_parse_markdown_v3("`\n`a", "\na", {{td::MessageEntity::Type::Code, 0, 1}}, true);
+  check_parse_markdown_v3("``", "``", {});
+  check_parse_markdown_v3("`a````b```", "`a````b```", {});
+  check_parse_markdown_v3("ab", {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Pre, 1, 1}}, "ab",
+                          {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Pre, 1, 1}});
+
+  check_parse_markdown_v3("[a](b[c](t.me)", "[a](b[c](t.me)", {});
+  check_parse_markdown_v3("[](t.me)", "[](t.me)", {});
+  check_parse_markdown_v3("[ ](t.me)", " ", {{td::MessageEntity::Type::TextUrl, 0, 1, "http://t.me/"}});
+  check_parse_markdown_v3("[ ](t.me)", "", {}, true);
+  check_parse_markdown_v3("[ ](t.me)a", " a", {{td::MessageEntity::Type::TextUrl, 0, 1, "http://t.me/"}}, true);
+  check_parse_markdown_v3(
+      "[ ](t.me) [ ](t.me)",
+      {{td::MessageEntity::Type::TextUrl, 8, 1, "http://t.me/"}, {10, 1, td::UserId(static_cast(1))}},
+      "[ ](t.me) [ ](t.me)",
+      {{td::MessageEntity::Type::TextUrl, 8, 1, "http://t.me/"}, {10, 1, td::UserId(static_cast(1))}});
+  check_parse_markdown_v3("[\n](t.me)", "\n", {{td::MessageEntity::Type::TextUrl, 0, 1, "http://t.me/"}});
+  check_parse_markdown_v3("[\n](t.me)a", "\na", {{td::MessageEntity::Type::TextUrl, 0, 1, "http://t.me/"}}, true);
+  check_parse_markdown_v3("asd[abcd](google.com)", {{td::MessageEntity::Type::Italic, 0, 5}}, "asdabcd",
+                          {{td::MessageEntity::Type::Italic, 0, 3},
+                           {td::MessageEntity::Type::TextUrl, 3, 4, "http://google.com/"},
+                           {td::MessageEntity::Type::Italic, 3, 1}});
+  check_parse_markdown_v3("asd[abcd](google.com)efg[hi](https://t.me?t=1#h)e",
+                          {{td::MessageEntity::Type::Italic, 0, 5}, {td::MessageEntity::Type::Italic, 18, 31}},
+                          "asdabcdefghie",
+                          {{td::MessageEntity::Type::Italic, 0, 3},
+                           {td::MessageEntity::Type::TextUrl, 3, 4, "http://google.com/"},
+                           {td::MessageEntity::Type::Italic, 3, 1},
+                           {td::MessageEntity::Type::Italic, 7, 3},
+                           {td::MessageEntity::Type::TextUrl, 10, 2, "https://t.me/?t=1#h"},
+                           {td::MessageEntity::Type::Italic, 10, 2},
+                           {td::MessageEntity::Type::Italic, 12, 1}});
+  check_parse_markdown_v3(
+      "🏟🏟🏟[🏟🏟🏟🏟🏟](www.🤙.tk#1)🤙🤙🤙[🏟🏟🏟🏟](www.🤙.tk#2)🤙🤙🤙["
+      "🏟🏟🏟🏟](www.🤙.tk#3)🏟🏟🏟[🏟🏟🏟🏟](www.🤙.tk#4)🤙🤙",
+      "🏟🏟🏟🏟🏟🏟🏟🏟🤙🤙🤙🏟🏟🏟🏟🤙🤙🤙🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟"
+      "🏟🤙🤙",
+      {{td::MessageEntity::Type::TextUrl, 6, 10, "http://www.🤙.tk/#1"},
+       {td::MessageEntity::Type::TextUrl, 22, 8, "http://www.🤙.tk/#2"},
+       {td::MessageEntity::Type::TextUrl, 36, 8, "http://www.🤙.tk/#3"},
+       {td::MessageEntity::Type::TextUrl, 50, 8, "http://www.🤙.tk/#4"}});
+  check_parse_markdown_v3(
+      "[🏟🏟🏟🏟🏟](www.🤙.tk#1)[🏟🏟🏟🏟](www.🤙.tk#2)[🏟🏟🏟🏟](www.🤙.tk#3)["
+      "🏟🏟🏟🏟](www.🤙.tk#4)",
+      "🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟",
+      {{td::MessageEntity::Type::TextUrl, 0, 10, "http://www.🤙.tk/#1"},
+       {td::MessageEntity::Type::TextUrl, 10, 8, "http://www.🤙.tk/#2"},
+       {td::MessageEntity::Type::TextUrl, 18, 8, "http://www.🤙.tk/#3"},
+       {td::MessageEntity::Type::TextUrl, 26, 8, "http://www.🤙.tk/#4"}});
+  check_parse_markdown_v3(
+      "🏟🏟🏟[🏟🏟🏟🏟🏟](www.🤙.tk)🤙🤙🤙[🏟🏟🏟🏟](www.🤙.tk)🤙🤙🤙["
+      "🏟🏟🏟🏟](www.🤙.tk)🏟🏟🏟[🏟🏟🏟🏟](www.🤙.tk)🤙🤙",
+      {{td::MessageEntity::Type::Bold, 0, 2},
+       {td::MessageEntity::Type::Bold, 4, 2},
+       {td::MessageEntity::Type::Bold, 7, 2},
+       {td::MessageEntity::Type::Bold, 11, 2},
+       {td::MessageEntity::Type::Bold, 15, 2},
+       {td::MessageEntity::Type::Bold, 18, 2},
+       {td::MessageEntity::Type::Bold, 26, 2},
+       {31, 2, td::UserId(static_cast(1))},
+       {td::MessageEntity::Type::Bold, 35, 1},
+       {td::MessageEntity::Type::Bold, 44, 2},
+       {td::MessageEntity::Type::Bold, 50, 2},
+       {td::MessageEntity::Type::Bold, 54, 2},
+       {56, 2, td::UserId(static_cast(2))},
+       {td::MessageEntity::Type::Bold, 58, 7},
+       {60, 2, td::UserId(static_cast(3))},
+       {td::MessageEntity::Type::Bold, 67, 7},
+       {td::MessageEntity::Type::Bold, 80, 7},
+       {td::MessageEntity::Type::Bold, 89, 25}},
+      "🏟🏟🏟🏟🏟🏟🏟🏟🤙🤙🤙🏟🏟🏟🏟🤙🤙🤙🏟🏟🏟🏟🏟🏟🏟🏟🏟🏟"
+      "🏟🤙🤙",
+      {{td::MessageEntity::Type::Bold, 0, 2},
+       {td::MessageEntity::Type::Bold, 4, 2},
+       {td::MessageEntity::Type::TextUrl, 6, 10, "http://www.🤙.tk/"},
+       {td::MessageEntity::Type::Bold, 6, 2},
+       {td::MessageEntity::Type::Bold, 10, 2},
+       {td::MessageEntity::Type::Bold, 14, 2},
+       {18, 2, td::UserId(static_cast(1))},
+       {td::MessageEntity::Type::TextUrl, 22, 8, "http://www.🤙.tk/"},
+       {30, 2, td::UserId(static_cast(2))},
+       {td::MessageEntity::Type::Bold, 32, 2},
+       {34, 2, td::UserId(static_cast(3))},
+       {td::MessageEntity::Type::Bold, 34, 2},
+       {td::MessageEntity::Type::TextUrl, 36, 8, "http://www.🤙.tk/"},
+       {td::MessageEntity::Type::Bold, 36, 2},
+       {td::MessageEntity::Type::Bold, 40, 4},
+       {td::MessageEntity::Type::Bold, 44, 4},
+       {td::MessageEntity::Type::TextUrl, 50, 8, "http://www.🤙.tk/"},
+       {td::MessageEntity::Type::Bold, 50, 8},
+       {td::MessageEntity::Type::Bold, 58, 4}});
+  check_parse_markdown_v3("[`a`](t.me) [b](t.me)", {{td::MessageEntity::Type::Code, 13, 1}}, "[a](t.me) [b](t.me)",
+                          {{td::MessageEntity::Type::Code, 1, 1}, {td::MessageEntity::Type::Code, 11, 1}});
+  check_parse_markdown_v3(
+      "[text](example.com)",
+      {{td::MessageEntity::Type::Strikethrough, 0, 1}, {td::MessageEntity::Type::Strikethrough, 5, 14}}, "text",
+      {{td::MessageEntity::Type::TextUrl, 0, 4, "http://example.com/"}});
+  check_parse_markdown_v3("[text](example.com)",
+                          {{td::MessageEntity::Type::Spoiler, 0, 1}, {td::MessageEntity::Type::Spoiler, 5, 14}}, "text",
+                          {{td::MessageEntity::Type::TextUrl, 0, 4, "http://example.com/"}});
+
+  check_parse_markdown_v3("🏟[🏟](t.me) `🏟` [🏟](t.me) `a`", "🏟🏟 🏟 🏟 a",
+                          {{td::MessageEntity::Type::TextUrl, 2, 2, "http://t.me/"},
+                           {td::MessageEntity::Type::Code, 5, 2},
+                           {td::MessageEntity::Type::TextUrl, 8, 2, "http://t.me/"},
+                           {td::MessageEntity::Type::Code, 11, 1}});
+
+  check_parse_markdown_v3("__ __", " ", {{td::MessageEntity::Type::Italic, 0, 1}});
+  check_parse_markdown_v3("__\n__", "\n", {{td::MessageEntity::Type::Italic, 0, 1}});
+  check_parse_markdown_v3("__ __a", " a", {}, true);
+  check_parse_markdown_v3("__\n__a", "\na", {}, true);
+  check_parse_markdown_v3("**** __a__ **b** ~~c~~ ||d||", "**** a b c d",
+                          {{td::MessageEntity::Type::Italic, 5, 1},
+                           {td::MessageEntity::Type::Bold, 7, 1},
+                           {td::MessageEntity::Type::Strikethrough, 9, 1},
+                           {td::MessageEntity::Type::Spoiler, 11, 1}});
+  check_parse_markdown_v3("тест __аааа__ **бббб** ~~вввв~~ ||гггг||", "тест аааа бббб вввв гггг",
+                          {{td::MessageEntity::Type::Italic, 5, 4},
+                           {td::MessageEntity::Type::Bold, 10, 4},
+                           {td::MessageEntity::Type::Strikethrough, 15, 4},
+                           {td::MessageEntity::Type::Spoiler, 20, 4}});
+  check_parse_markdown_v3("___a___ ***b** ~c~~", "___a___ ***b** ~c~~", {});
+  check_parse_markdown_v3(
+      "__asd[ab__cd](t.me)", "asdabcd",
+      {{td::MessageEntity::Type::Italic, 0, 5}, {td::MessageEntity::Type::TextUrl, 3, 4, "http://t.me/"}});
+  check_parse_markdown_v3("__asd[ab__cd](t.me)", "asdabcd",
+                          {{td::MessageEntity::Type::Italic, 0, 3},
+                           {td::MessageEntity::Type::TextUrl, 3, 4, "http://t.me/"},
+                           {td::MessageEntity::Type::Italic, 3, 2}},
+                          true);
+  check_parse_markdown_v3("__a #test__test", "__a #test__test", {});
+  check_parse_markdown_v3("a #testtest", {{td::MessageEntity::Type::Italic, 0, 7}}, "a #testtest",
+                          {{td::MessageEntity::Type::Italic, 0, 7}});
+
+  // TODO parse_markdown_v3 is not idempotent now, which is bad
+  check_parse_markdown_v3(
+      "~~**~~__**a__", {{td::MessageEntity::Type::Strikethrough, 2, 1}, {td::MessageEntity::Type::Bold, 6, 1}},
+      "**__**a__", {{td::MessageEntity::Type::Strikethrough, 0, 2}, {td::MessageEntity::Type::Bold, 2, 1}}, true);
+  check_parse_markdown_v3("**__**a__",
+                          {{td::MessageEntity::Type::Strikethrough, 0, 2}, {td::MessageEntity::Type::Bold, 2, 1}},
+                          "__a__", {{td::MessageEntity::Type::Bold, 0, 2}}, true);
+  check_parse_markdown_v3("__a__", {{td::MessageEntity::Type::Bold, 0, 2}}, "a",
+                          {{td::MessageEntity::Type::Italic, 0, 1}}, true);
+  check_parse_markdown_v3("~~__~~#test__test", "__#test__test", {{td::MessageEntity::Type::Strikethrough, 0, 2}});
+  check_parse_markdown_v3("__#test__test", {{td::MessageEntity::Type::Strikethrough, 0, 2}}, "#testtest",
+                          {{td::MessageEntity::Type::Italic, 0, 5}});
+
+  check_parse_markdown_v3(
+      "~~**~~||**a||", {{td::MessageEntity::Type::Strikethrough, 2, 1}, {td::MessageEntity::Type::Bold, 6, 1}},
+      "**||**a||", {{td::MessageEntity::Type::Strikethrough, 0, 2}, {td::MessageEntity::Type::Bold, 2, 1}}, true);
+  check_parse_markdown_v3("**||**a||",
+                          {{td::MessageEntity::Type::Strikethrough, 0, 2}, {td::MessageEntity::Type::Bold, 2, 1}},
+                          "||a||", {{td::MessageEntity::Type::Bold, 0, 2}}, true);
+  check_parse_markdown_v3("||a||", {{td::MessageEntity::Type::Bold, 0, 2}}, "a",
+                          {{td::MessageEntity::Type::Spoiler, 0, 1}}, true);
+  check_parse_markdown_v3("~~||~~#test||test", "#testtest", {{td::MessageEntity::Type::Spoiler, 0, 5}});
+  check_parse_markdown_v3("||#test||test", {{td::MessageEntity::Type::Strikethrough, 0, 2}}, "#testtest",
+                          {{td::MessageEntity::Type::Spoiler, 0, 5}});
+
+  check_parse_markdown_v3("__[ab_](t.me)_", "__ab__", {{td::MessageEntity::Type::TextUrl, 2, 3, "http://t.me/"}});
+  check_parse_markdown_v3(
+      "__[ab__](t.me)_", "ab_",
+      {{td::MessageEntity::Type::TextUrl, 0, 2, "http://t.me/"}, {td::MessageEntity::Type::Italic, 0, 2}});
+  check_parse_markdown_v3("__[__ab__](t.me)__", "____ab____",
+                          {{td::MessageEntity::Type::TextUrl, 2, 6, "http://t.me/"}});
+  check_parse_markdown_v3(
+      "__[__ab__](t.me)a__", "____aba",
+      {{td::MessageEntity::Type::TextUrl, 2, 4, "http://t.me/"}, {td::MessageEntity::Type::Italic, 6, 1}});
+  check_parse_markdown_v3("`a` __ab__", {{td::MessageEntity::Type::Bold, 6, 3}}, "a __ab__",
+                          {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Bold, 4, 3}});
+  check_parse_markdown_v3("`a` __ab__", {{td::MessageEntity::Type::Underline, 5, 1}}, "a __ab__",
+                          {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Underline, 3, 1}});
+
+  check_parse_markdown_v3("||[ab|](t.me)|", "||ab||", {{td::MessageEntity::Type::TextUrl, 2, 3, "http://t.me/"}});
+  check_parse_markdown_v3(
+      "||[ab||](t.me)|", "ab|",
+      {{td::MessageEntity::Type::TextUrl, 0, 2, "http://t.me/"}, {td::MessageEntity::Type::Spoiler, 0, 2}});
+  check_parse_markdown_v3("||[||ab||](t.me)||", "||||ab||||",
+                          {{td::MessageEntity::Type::TextUrl, 2, 6, "http://t.me/"}});
+  check_parse_markdown_v3(
+      "||[||ab||](t.me)a||", "||||aba",
+      {{td::MessageEntity::Type::TextUrl, 2, 4, "http://t.me/"}, {td::MessageEntity::Type::Spoiler, 6, 1}});
+  check_parse_markdown_v3("`a` ||ab||", {{td::MessageEntity::Type::Bold, 6, 3}}, "a ||ab||",
+                          {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Bold, 4, 3}});
+  check_parse_markdown_v3("`a` ||ab||", {{td::MessageEntity::Type::Underline, 5, 1}}, "a ||ab||",
+                          {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Underline, 3, 1}});
+
+  check_parse_markdown_v3("`a` @test__test__test", "a @test__test__test", {{td::MessageEntity::Type::Code, 0, 1}});
+  check_parse_markdown_v3("`a` #test__test__test", "a #test__test__test", {{td::MessageEntity::Type::Code, 0, 1}});
+  check_parse_markdown_v3("`a` __@test_test_test__", "a @test_test_test",
+                          {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Italic, 2, 15}});
+  check_parse_markdown_v3("`a` __#test_test_test__", "a #test_test_test",
+                          {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Italic, 2, 15}});
+  check_parse_markdown_v3("[a](t.me) __@test**test**test__", "a @testtesttest",
+                          {{td::MessageEntity::Type::TextUrl, 0, 1, "http://t.me/"},
+                           {td::MessageEntity::Type::Italic, 2, 13},
+                           {td::MessageEntity::Type::Bold, 7, 4}});
+  check_parse_markdown_v3("[a](t.me) __#test~~test~~test__", "a #testtesttest",
+                          {{td::MessageEntity::Type::TextUrl, 0, 1, "http://t.me/"},
+                           {td::MessageEntity::Type::Italic, 2, 13},
+                           {td::MessageEntity::Type::Strikethrough, 7, 4}});
+  check_parse_markdown_v3("[a](t.me) __@test__test__test__", "a @testtesttest",
+                          {{td::MessageEntity::Type::TextUrl, 0, 1, "http://t.me/"},
+                           {td::MessageEntity::Type::Italic, 2, 5},
+                           {td::MessageEntity::Type::Italic, 11, 4}});
+  check_parse_markdown_v3("__**~~__gh**~~", "gh",
+                          {{td::MessageEntity::Type::Bold, 0, 2}, {td::MessageEntity::Type::Strikethrough, 0, 2}});
+  check_parse_markdown_v3("__ab**cd~~ef__gh**ij~~", "abcdefghij",
+                          {{td::MessageEntity::Type::Italic, 0, 6},
+                           {td::MessageEntity::Type::Bold, 2, 6},
+                           {td::MessageEntity::Type::Strikethrough, 4, 6}});
+  check_parse_markdown_v3("__ab**cd~~ef||gh__ij**kl~~mn||", "abcdefghijklmn",
+                          {{td::MessageEntity::Type::Italic, 0, 2},
+                           {td::MessageEntity::Type::Bold, 2, 2},
+                           {td::MessageEntity::Type::Italic, 2, 2},
+                           {td::MessageEntity::Type::Bold, 4, 2},
+                           {td::MessageEntity::Type::Italic, 4, 2},
+                           {td::MessageEntity::Type::Strikethrough, 4, 2},
+                           {td::MessageEntity::Type::Spoiler, 6, 8},
+                           {td::MessageEntity::Type::Strikethrough, 6, 6},
+                           {td::MessageEntity::Type::Bold, 6, 4},
+                           {td::MessageEntity::Type::Italic, 6, 2}},
+                          true);
+  check_parse_markdown_v3("__ab**[cd~~ef__](t.me)gh**ij~~", "abcdefghij",
+                          {{td::MessageEntity::Type::Italic, 0, 6},
+                           {td::MessageEntity::Type::Bold, 2, 6},
+                           {td::MessageEntity::Type::TextUrl, 2, 4, "http://t.me/"},
+                           {td::MessageEntity::Type::Strikethrough, 4, 6}});
+  check_parse_markdown_v3("__ab**[cd~~e](t.me)f__gh**ij~~", "abcdefghij",
+                          {{td::MessageEntity::Type::Italic, 0, 6},
+                           {td::MessageEntity::Type::Bold, 2, 6},
+                           {td::MessageEntity::Type::TextUrl, 2, 3, "http://t.me/"},
+                           {td::MessageEntity::Type::Strikethrough, 4, 6}});
+  check_parse_markdown_v3("__ab**[cd~~](t.me)ef__gh**ij~~", "abcdefghij",
+                          {{td::MessageEntity::Type::Italic, 0, 6},
+                           {td::MessageEntity::Type::Bold, 2, 6},
+                           {td::MessageEntity::Type::TextUrl, 2, 2, "http://t.me/"},
+                           {td::MessageEntity::Type::Strikethrough, 4, 6}});
+  check_parse_markdown_v3("[__**bold italic link**__](example.com)", "bold italic link",
+                          {{td::MessageEntity::Type::TextUrl, 0, 16, "http://example.com/"},
+                           {td::MessageEntity::Type::Bold, 0, 16},
+                           {td::MessageEntity::Type::Italic, 0, 16}});
+  check_parse_markdown_v3(
+      "__italic__ ~~strikethrough~~ **bold** `code` ```pre``` __[italic__ text_url](telegram.org) __italic**bold "
+      "italic__bold**__italic__ ~~strikethrough~~ **bold** `code` ```pre``` __[italic__ text_url](telegram.org) "
+      "__italic**bold italic__bold** ||spoiler||",
+      "italic strikethrough bold code pre italic text_url italicbold italicbolditalic strikethrough bold code pre "
+      "italic text_url italicbold italicbold spoiler",
+      {{td::MessageEntity::Type::Italic, 0, 6},
+       {td::MessageEntity::Type::Strikethrough, 7, 13},
+       {td::MessageEntity::Type::Bold, 21, 4},
+       {td::MessageEntity::Type::Code, 26, 4},
+       {td::MessageEntity::Type::Pre, 31, 3},
+       {td::MessageEntity::Type::TextUrl, 35, 15, "http://telegram.org/"},
+       {td::MessageEntity::Type::Italic, 35, 6},
+       {td::MessageEntity::Type::Italic, 51, 17},
+       {td::MessageEntity::Type::Bold, 57, 15},
+       {td::MessageEntity::Type::Italic, 72, 6},
+       {td::MessageEntity::Type::Strikethrough, 79, 13},
+       {td::MessageEntity::Type::Bold, 93, 4},
+       {td::MessageEntity::Type::Code, 98, 4},
+       {td::MessageEntity::Type::Pre, 103, 3},
+       {td::MessageEntity::Type::TextUrl, 107, 15, "http://telegram.org/"},
+       {td::MessageEntity::Type::Italic, 107, 6},
+       {td::MessageEntity::Type::Italic, 123, 17},
+       {td::MessageEntity::Type::Bold, 129, 15},
+       {td::MessageEntity::Type::Spoiler, 145, 7}});
+
+  td::vector parts{"a", " #test__a", "__", "**", "~~", "||", "[", "](t.me)", "`"};
+  td::vector types{
+      td::MessageEntity::Type::Bold,          td::MessageEntity::Type::Italic,  td::MessageEntity::Type::Underline,
+      td::MessageEntity::Type::Strikethrough, td::MessageEntity::Type::Spoiler, td::MessageEntity::Type::Code,
+      td::MessageEntity::Type::Pre,           td::MessageEntity::Type::PreCode, td::MessageEntity::Type::TextUrl,
+      td::MessageEntity::Type::MentionName,   td::MessageEntity::Type::Cashtag};
+  for (size_t test_n = 0; test_n < 1000; test_n++) {
+    td::string str;
+    int part_n = td::Random::fast(1, 200);
+    for (int i = 0; i < part_n; i++) {
+      str += parts[td::Random::fast(0, static_cast(parts.size()) - 1)];
+    }
+    td::vector entities;
+    int entity_n = td::Random::fast(1, 20);
+    for (int i = 0; i < entity_n; i++) {
+      auto type = types[td::Random::fast(0, static_cast(types.size()) - 1)];
+      td::int32 offset = td::Random::fast(0, static_cast(str.size()) - 1);
+      auto max_length = static_cast(str.size() - offset);
+      if ((test_n & 1) != 0 && max_length > 4) {
+        max_length = 4;
+      }
+      td::int32 length = td::Random::fast(0, max_length);
+      entities.emplace_back(type, offset, length);
+    }
+
+    td::FormattedText text{std::move(str), std::move(entities)};
+    while (true) {
+      ASSERT_TRUE(td::fix_formatted_text(text.text, text.entities, true, true, true, true, true).is_ok());
+      auto parsed_text = td::parse_markdown_v3(text);
+      ASSERT_TRUE(td::fix_formatted_text(parsed_text.text, parsed_text.entities, true, true, true, true, true).is_ok());
+      if (parsed_text == text) {
+        break;
+      }
+      text = std::move(parsed_text);
+    }
+    ASSERT_EQ(text, td::parse_markdown_v3(text));
+    auto markdown_text = td::get_markdown_v3(text);
+    ASSERT_TRUE(text == markdown_text || text == td::parse_markdown_v3(markdown_text));
+  }
+}
+
+static void check_get_markdown_v3(const td::string &result_text, const td::vector &result_entities,
+                                  td::string text, td::vector entities) {
+  auto markdown_text = td::get_markdown_v3({std::move(text), std::move(entities)});
+  ASSERT_STREQ(result_text, markdown_text.text);
+  ASSERT_EQ(result_entities, markdown_text.entities);
+}
+
+TEST(MessageEntities, get_markdown_v3) {
+  check_get_markdown_v3("``` ```", {}, " ", {{td::MessageEntity::Type::Pre, 0, 1}});
+  check_get_markdown_v3("` `", {}, " ", {{td::MessageEntity::Type::Code, 0, 1}});
+  check_get_markdown_v3("`\n`", {}, "\n", {{td::MessageEntity::Type::Code, 0, 1}});
+  check_get_markdown_v3("ab", {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Pre, 1, 1}}, "ab",
+                        {{td::MessageEntity::Type::Code, 0, 1}, {td::MessageEntity::Type::Pre, 1, 1}});
+
+  check_get_markdown_v3("[ ](http://t.me/)", {}, " ", {{td::MessageEntity::Type::TextUrl, 0, 1, "http://t.me/"}});
+  check_get_markdown_v3(
+      "[ ]t.me[)](http://t.me/) [ ](t.me)", {{25, 1, td::UserId(static_cast(1))}}, "[ ]t.me) [ ](t.me)",
+      {{td::MessageEntity::Type::TextUrl, 7, 1, "http://t.me/"}, {9, 1, td::UserId(static_cast(1))}});
+
+  check_get_markdown_v3("__ __", {}, " ", {{td::MessageEntity::Type::Italic, 0, 1}});
+  check_get_markdown_v3("** **", {}, " ", {{td::MessageEntity::Type::Bold, 0, 1}});
+  check_get_markdown_v3("~~ ~~", {}, " ", {{td::MessageEntity::Type::Strikethrough, 0, 1}});
+  check_get_markdown_v3("|| ||", {}, " ", {{td::MessageEntity::Type::Spoiler, 0, 1}});
+  check_get_markdown_v3("__a__ **b** ~~c~~ ||d|| e", {{td::MessageEntity::Type::PreCode, 24, 1, "C++"}}, "a b c d e",
+                        {{td::MessageEntity::Type::Italic, 0, 1},
+                         {td::MessageEntity::Type::Bold, 2, 1},
+                         {td::MessageEntity::Type::Strikethrough, 4, 1},
+                         {td::MessageEntity::Type::Spoiler, 6, 1},
+                         {td::MessageEntity::Type::PreCode, 8, 1, "C++"}});
+  check_get_markdown_v3("`ab` ```cd``` ef", {{td::MessageEntity::Type::PreCode, 14, 2, "C++"}}, "ab cd ef",
+                        {{td::MessageEntity::Type::Code, 0, 2},
+                         {td::MessageEntity::Type::Pre, 3, 2},
+                         {td::MessageEntity::Type::PreCode, 6, 2, "C++"}});
+  check_get_markdown_v3("__asd__[__ab__cd](http://t.me/)", {}, "asdabcd",
+                        {{td::MessageEntity::Type::Italic, 0, 3},
+                         {td::MessageEntity::Type::TextUrl, 3, 4, "http://t.me/"},
+                         {td::MessageEntity::Type::Italic, 3, 2}});
+
+  check_get_markdown_v3("__ab", {{td::MessageEntity::Type::Italic, 3, 1}}, "__ab",
+                        {{td::MessageEntity::Type::Italic, 3, 1}});
+  check_get_markdown_v3("__ab__**__cd__**~~**__ef__gh**ij~~", {}, "abcdefghij",
+                        {{td::MessageEntity::Type::Italic, 0, 2},
+                         {td::MessageEntity::Type::Bold, 2, 2},
+                         {td::MessageEntity::Type::Italic, 2, 2},
+                         {td::MessageEntity::Type::Strikethrough, 4, 6},
+                         {td::MessageEntity::Type::Bold, 4, 4},
+                         {td::MessageEntity::Type::Italic, 4, 2}});
+  check_get_markdown_v3("[**__bold italic link__**](http://example.com/)", {}, "bold italic link",
+                        {{td::MessageEntity::Type::TextUrl, 0, 16, "http://example.com/"},
+                         {td::MessageEntity::Type::Bold, 0, 16},
+                         {td::MessageEntity::Type::Italic, 0, 16}});
 }
diff --git a/protocols/Telegram/tdlib/td/test/mtproto.cpp b/protocols/Telegram/tdlib/td/test/mtproto.cpp
index 7702a1e37b..89f5441ab5 100644
--- a/protocols/Telegram/tdlib/td/test/mtproto.cpp
+++ b/protocols/Telegram/tdlib/td/test/mtproto.cpp
@@ -1,143 +1,276 @@
 //
-// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022
 //
 // Distributed under the Boost Software License, Version 1.0. (See accompanying
 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
 //
-#include "td/utils/tests.h"
-
-#include "td/actor/actor.h"
-#include "td/actor/PromiseFuture.h"
+#include "td/telegram/ConfigManager.h"
+#include "td/telegram/net/DcId.h"
+#include "td/telegram/net/PublicRsaKeyShared.h"
+#include "td/telegram/net/Session.h"
+#include "td/telegram/NotificationManager.h"
 
-#include "td/mtproto/crypto.h"
+#include "td/mtproto/AuthData.h"
+#include "td/mtproto/DhCallback.h"
+#include "td/mtproto/DhHandshake.h"
 #include "td/mtproto/Handshake.h"
 #include "td/mtproto/HandshakeActor.h"
-#include "td/mtproto/HandshakeConnection.h"
+#include "td/mtproto/Ping.h"
 #include "td/mtproto/PingConnection.h"
+#include "td/mtproto/ProxySecret.h"
 #include "td/mtproto/RawConnection.h"
+#include "td/mtproto/RSA.h"
+#include "td/mtproto/TlsInit.h"
+#include "td/mtproto/TransportType.h"
 
+#include "td/net/GetHostByNameActor.h"
 #include "td/net/Socks5.h"
+#include "td/net/TransparentProxy.h"
 
-#include "td/telegram/ConfigManager.h"
-#include "td/telegram/net/PublicRsaKeyShared.h"
+#include "td/actor/actor.h"
+#include "td/actor/ConcurrentScheduler.h"
 
+#include "td/utils/base64.h"
+#include "td/utils/BufferedFd.h"
+#include "td/utils/common.h"
+#include "td/utils/crypto.h"
 #include "td/utils/logging.h"
+#include "td/utils/port/Clocks.h"
 #include "td/utils/port/IPAddress.h"
 #include "td/utils/port/SocketFd.h"
+#include "td/utils/Promise.h"
+#include "td/utils/Random.h"
+#include "td/utils/Slice.h"
+#include "td/utils/SliceBuilder.h"
 #include "td/utils/Status.h"
+#include "td/utils/tests.h"
+#include "td/utils/Time.h"
 
-REGISTER_TESTS(mtproto);
-
-using namespace td;
-using namespace mtproto;
-
-#if !TD_WINDOWS && !TD_EMSCRIPTEN  // TODO
-TEST(Mtproto, config) {
-  ConcurrentScheduler sched;
-  int threads_n = 0;
-  sched.init(threads_n);
+TEST(Mtproto, GetHostByNameActor) {
+  int threads_n = 1;
+  td::ConcurrentScheduler sched(threads_n, 0);
 
-  int cnt = 3;
+  int cnt = 1;
+  td::vector> actors;
   {
-    auto guard = sched.get_current_guard();
-    get_simple_config_azure(PromiseCreator::lambda([&](Result r_simple_config) {
-      LOG(ERROR) << to_string(r_simple_config.ok());
-      if (--cnt == 0) {
-        Scheduler::instance()->finish();
-      }
-    }))
-        .release();
-
-    get_simple_config_google_app(PromiseCreator::lambda([&](Result r_simple_config) {
-      LOG(ERROR) << to_string(r_simple_config.ok());
-      if (--cnt == 0) {
-        Scheduler::instance()->finish();
-      }
-    }))
-        .release();
+    auto guard = sched.get_main_guard();
+
+    auto run = [&](td::ActorId actor_id, td::string host, bool prefer_ipv6, bool allow_ok,
+                   bool allow_error) {
+      auto promise = td::PromiseCreator::lambda([&cnt, &actors, num = cnt, host, allow_ok,
+                                                 allow_error](td::Result r_ip_address) {
+        if (r_ip_address.is_error() && !allow_error) {
+          LOG(ERROR) << num << " \"" << host << "\" " << r_ip_address.error();
+        }
+        if (r_ip_address.is_ok() && !allow_ok && (r_ip_address.ok().is_ipv6() || r_ip_address.ok().get_ipv4() != 0)) {
+          LOG(ERROR) << num << " \"" << host << "\" " << r_ip_address.ok();
+        }
+        if (--cnt == 0) {
+          actors.clear();
+          td::Scheduler::instance()->finish();
+        }
+      });
+      cnt++;
+      td::send_closure_later(actor_id, &td::GetHostByNameActor::run, host, 443, prefer_ipv6, std::move(promise));
+    };
 
-    get_simple_config_google_dns(PromiseCreator::lambda([&](Result r_simple_config) {
-      LOG(ERROR) << to_string(r_simple_config.ok());
-      if (--cnt == 0) {
-        Scheduler::instance()->finish();
+    td::vector hosts = {"127.0.0.2",
+                                    "1.1.1.1",
+                                    "localhost",
+                                    "web.telegram.org",
+                                    "web.telegram.org.",
+                                    "москва.рф",
+                                    "",
+                                    "%",
+                                    " ",
+                                    "a",
+                                    "\x80",
+                                    "[]",
+                                    "127.0.0.1.",
+                                    "0x12.0x34.0x56.0x78",
+                                    "0x7f.001",
+                                    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+                                    "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
+                                    "[[2001:0db8:85a3:0000:0000:8a2e:0370:7334]]"};
+    for (const auto &types :
+         {td::vector{td::GetHostByNameActor::ResolverType::Native},
+          td::vector{td::GetHostByNameActor::ResolverType::Google},
+          td::vector{td::GetHostByNameActor::ResolverType::Google,
+                                                           td::GetHostByNameActor::ResolverType::Google,
+                                                           td::GetHostByNameActor::ResolverType::Native}}) {
+      td::GetHostByNameActor::Options options;
+      options.resolver_types = types;
+      options.scheduler_id = threads_n;
+
+      auto actor = td::create_actor("GetHostByNameActor", std::move(options));
+      auto actor_id = actor.get();
+      actors.push_back(std::move(actor));
+
+      for (auto host : hosts) {
+        for (auto prefer_ipv6 : {false, true}) {
+          bool allow_ok = host.size() > 2 && host[1] != '[';
+          bool allow_both = host == "127.0.0.1." || host == "localhost" || (host == "москва.рф" && prefer_ipv6);
+          bool allow_error = !allow_ok || allow_both;
+          run(actor_id, host, prefer_ipv6, allow_ok, allow_error);
+        }
       }
-    }))
-        .release();
+    }
   }
+  cnt--;
   sched.start();
   while (sched.run_main(10)) {
-    // empty;
+    // empty
   }
   sched.finish();
 }
-#endif
+
+TEST(Time, to_unix_time) {
+  ASSERT_EQ(0, td::HttpDate::to_unix_time(1970, 1, 1, 0, 0, 0).move_as_ok());
+  ASSERT_EQ(60 * 60 + 60 + 1, td::HttpDate::to_unix_time(1970, 1, 1, 1, 1, 1).move_as_ok());
+  ASSERT_EQ(24 * 60 * 60, td::HttpDate::to_unix_time(1970, 1, 2, 0, 0, 0).move_as_ok());
+  ASSERT_EQ(31 * 24 * 60 * 60, td::HttpDate::to_unix_time(1970, 2, 1, 0, 0, 0).move_as_ok());
+  ASSERT_EQ(365 * 24 * 60 * 60, td::HttpDate::to_unix_time(1971, 1, 1, 0, 0, 0).move_as_ok());
+  ASSERT_EQ(1562780559, td::HttpDate::to_unix_time(2019, 7, 10, 17, 42, 39).move_as_ok());
+}
+
+TEST(Time, parse_http_date) {
+  ASSERT_EQ(784887151, td::HttpDate::parse_http_date("Tue, 15 Nov 1994 08:12:31 GMT").move_as_ok());
+}
+
+TEST(Mtproto, config) {
+  int threads_n = 0;
+  td::ConcurrentScheduler sched(threads_n, 0);
+
+  int cnt = 1;
+  {
+    auto guard = sched.get_main_guard();
+
+    auto run = [&](auto &func, bool is_test) {
+      auto promise =
+          td::PromiseCreator::lambda([&, num = cnt](td::Result r_simple_config_result) {
+            if (r_simple_config_result.is_ok()) {
+              auto simple_config_result = r_simple_config_result.move_as_ok();
+              auto date = simple_config_result.r_http_date.is_ok()
+                              ? td::to_string(simple_config_result.r_http_date.ok())
+                              : (PSTRING() << simple_config_result.r_http_date.error());
+              auto config = simple_config_result.r_config.is_ok()
+                                ? to_string(simple_config_result.r_config.ok())
+                                : (PSTRING() << simple_config_result.r_config.error());
+              LOG(ERROR) << num << " " << date << " " << config;
+            } else {
+              LOG(ERROR) << num << " " << r_simple_config_result.error();
+            }
+            if (--cnt == 0) {
+              td::Scheduler::instance()->finish();
+            }
+          });
+      cnt++;
+      func(std::move(promise), false, td::Slice(), is_test, -1).release();
+    };
+
+    run(td::get_simple_config_azure, false);
+    run(td::get_simple_config_google_dns, false);
+    run(td::get_simple_config_mozilla_dns, false);
+    run(td::get_simple_config_azure, true);
+    run(td::get_simple_config_google_dns, true);
+    run(td::get_simple_config_mozilla_dns, true);
+    run(td::get_simple_config_firebase_remote_config, false);
+    run(td::get_simple_config_firebase_realtime, false);
+    run(td::get_simple_config_firebase_firestore, false);
+  }
+  cnt--;
+  if (cnt != 0) {
+    sched.start();
+    while (sched.run_main(10)) {
+      // empty;
+    }
+    sched.finish();
+  }
+}
 
 TEST(Mtproto, encrypted_config) {
-  string data =
-      "   LQ2 \b\n\tru6xVXpHHckW4eQWK0X3uThupVOor5sXT8t298IjDksYeUseQTOIrnUqiQj7o"
-      "+ZgPfhnfe+lfcQA+naax9akgllimjlJtL5riTf3O7iqZSnJ614qmCucxqqVTbXk/"
-      "hY2KaJTtsMqk7cShJjM3aQ4DD40h2InTaG7uyVO2q7K0GMUTeY3AM0Rt1lUjKHLD"
-      "g4RwjTzZaG8TwfgL/mZ7jsvgTTTATPWKUo7SmxQ9Hsj+07NMGqr6JKZS6aiU1Knz"
-      "VGCZ3OJEyRYocktN4HjaLpkguilaHWlVM2UNFUd5a+ajfLIiiKlH0FRC3XZ12CND"
-      "Y+NBjv0I57N2O4fBfswTlA==  ";
-  auto config = decode_config(data).move_as_ok();
+  td::string data =
+      "   hO//tt \b\n\tiwPVovorKtIYtQ8y2ik7CqfJiJ4pJOCLRa4fBmNPixuRPXnBFF/3mTAAZoSyHq4SNylGHz0Cv1/"
+      "FnWWdEV+BPJeOTk+ARHcNkuJBt0CqnfcVCoDOpKqGyq0U31s2MOpQvHgAG+Tlpg02syuH0E4dCGRw5CbJPARiynteb9y5fT5x/"
+      "kmdp6BMR5tWQSQF0liH16zLh8BDSIdiMsikdcwnAvBwdNhRqQBqGx9MTh62MDmlebjtczE9Gz0z5cscUO2yhzGdphgIy6SP+"
+      "bwaqLWYF0XdPGjKLMUEJW+rou6fbL1t/EUXPtU0XmQAnO0Fh86h+AqDMOe30N4qKrPQ==   ";
+  auto config = td::decode_config(data).move_as_ok();
 }
 
-class TestPingActor : public Actor {
+class TestPingActor final : public td::Actor {
  public:
-  TestPingActor(IPAddress ip_address, Status *result) : ip_address_(ip_address), result_(result) {
+  TestPingActor(td::IPAddress ip_address, td::Status *result) : ip_address_(ip_address), result_(result) {
   }
 
  private:
-  IPAddress ip_address_;
-  std::unique_ptr ping_connection_;
-  Status *result_;
+  td::IPAddress ip_address_;
+  td::unique_ptr ping_connection_;
+  td::Status *result_;
+  bool is_inited_ = false;
 
-  void start_up() override {
-    ping_connection_ = std::make_unique(std::make_unique(
-        SocketFd::open(ip_address_).move_as_ok(), mtproto::TransportType::Tcp, nullptr));
+  void start_up() final {
+    auto r_socket = td::SocketFd::open(ip_address_);
+    if (r_socket.is_error()) {
+      LOG(ERROR) << "Failed to open socket: " << r_socket.error();
+      return stop();
+    }
 
-    ping_connection_->get_pollable().set_observer(this);
-    subscribe(ping_connection_->get_pollable());
+    ping_connection_ = td::mtproto::PingConnection::create_req_pq(
+        td::mtproto::RawConnection::create(
+            ip_address_, td::BufferedFd(r_socket.move_as_ok()),
+            td::mtproto::TransportType{td::mtproto::TransportType::Tcp, 0, td::mtproto::ProxySecret()}, nullptr),
+        3);
+
+    td::Scheduler::subscribe(ping_connection_->get_poll_info().extract_pollable_fd(this));
+    is_inited_ = true;
     set_timeout_in(10);
     yield();
   }
-  void tear_down() override {
-    unsubscribe_before_close(ping_connection_->get_pollable());
-    ping_connection_->close();
-    Scheduler::instance()->finish();
+
+  void tear_down() final {
+    if (is_inited_) {
+      td::Scheduler::unsubscribe_before_close(ping_connection_->get_poll_info().get_pollable_fd_ref());
+    }
+    td::Scheduler::instance()->finish();
   }
 
-  void loop() override {
+  void loop() final {
     auto status = ping_connection_->flush();
     if (status.is_error()) {
       *result_ = std::move(status);
       return stop();
     }
     if (ping_connection_->was_pong()) {
-      LOG(ERROR) << "GOT PONG";
+      LOG(INFO) << "GOT PONG";
       return stop();
     }
   }
 
-  void timeout_expired() override {
-    *result_ = Status::Error("Timeout expired");
+  void timeout_expired() final {
+    *result_ = td::Status::Error("Timeout expired");
     stop();
   }
 };
 
-static IPAddress get_default_ip_address() {
-  IPAddress ip_address;
+static td::IPAddress get_default_ip_address() {
+  td::IPAddress ip_address;
+#if TD_EMSCRIPTEN
+  ip_address.init_host_port("venus.web.telegram.org/apiws", 443).ensure();
+#else
   ip_address.init_ipv4_port("149.154.167.40", 80).ensure();
+#endif
   return ip_address;
 }
 
-class Mtproto_ping : public td::Test {
+static td::int32 get_default_dc_id() {
+  return 10002;
+}
+
+class Mtproto_ping final : public td::Test {
  public:
   using Test::Test;
   bool step() final {
     if (!is_inited_) {
-      sched_.init(0);
       sched_.create_actor_unsafe(0, "Pinger", get_default_ip_address(), &result_).release();
       sched_.start();
       is_inited_ = true;
@@ -151,57 +284,65 @@ class Mtproto_ping : public td::Test {
     if (result_.is_error()) {
       LOG(ERROR) << result_;
     }
-    ASSERT_TRUE(result_.is_ok());
     return false;
   }
 
  private:
   bool is_inited_ = false;
-  ConcurrentScheduler sched_;
-  Status result_;
+  td::ConcurrentScheduler sched_{0, 0};
+  td::Status result_;
 };
-Mtproto_ping mtproto_ping("Mtproto_ping");
+td::RegisterTest mtproto_ping("Mtproto_ping");
 
-class Context : public AuthKeyHandshakeContext {
+class HandshakeContext final : public td::mtproto::AuthKeyHandshakeContext {
  public:
-  DhCallback *get_dh_callback() override {
+  td::mtproto::DhCallback *get_dh_callback() final {
     return nullptr;
   }
-  PublicRsaKeyInterface *get_public_rsa_key_interface() override {
+  td::mtproto::PublicRsaKeyInterface *get_public_rsa_key_interface() final {
     return &public_rsa_key;
   }
 
  private:
-  PublicRsaKeyShared public_rsa_key{DcId::empty()};
+  td::PublicRsaKeyShared public_rsa_key{td::DcId::empty(), true};
 };
 
-class HandshakeTestActor : public Actor {
+class HandshakeTestActor final : public td::Actor {
  public:
-  explicit HandshakeTestActor(Status *result) : result_(result) {
+  HandshakeTestActor(td::int32 dc_id, td::Status *result) : dc_id_(dc_id), result_(result) {
   }
 
  private:
-  Status *result_;
+  td::int32 dc_id_ = 0;
+  td::Status *result_;
   bool wait_for_raw_connection_ = false;
-  std::unique_ptr raw_connection_;
+  td::unique_ptr raw_connection_;
   bool wait_for_handshake_ = false;
-  std::unique_ptr handshake_;
-  Status status_;
+  td::unique_ptr handshake_;
+  td::Status status_;
   bool wait_for_result_ = false;
 
-  void tear_down() override {
+  void tear_down() final {
     if (raw_connection_) {
       raw_connection_->close();
     }
-    finish(Status::Error("Interrupted"));
+    finish(td::Status::Error("Interrupted"));
   }
-  void loop() override {
+  void loop() final {
     if (!wait_for_raw_connection_ && !raw_connection_) {
-      raw_connection_ = std::make_unique(SocketFd::open(get_default_ip_address()).move_as_ok(),
-                                                                 mtproto::TransportType::Tcp, nullptr);
+      auto ip_address = get_default_ip_address();
+      auto r_socket = td::SocketFd::open(ip_address);
+      if (r_socket.is_error()) {
+        finish(td::Status::Error(PSTRING() << "Failed to open socket: " << r_socket.error()));
+        return stop();
+      }
+
+      raw_connection_ = td::mtproto::RawConnection::create(
+          ip_address, td::BufferedFd(r_socket.move_as_ok()),
+          td::mtproto::TransportType{td::mtproto::TransportType::Tcp, 0, td::mtproto::ProxySecret()}, nullptr);
     }
     if (!wait_for_handshake_ && !handshake_) {
-      handshake_ = std::make_unique(0);
+      handshake_ = td::make_unique(dc_id_, 3600);
     }
     if (raw_connection_ && handshake_) {
       if (wait_for_result_) {
@@ -211,34 +352,37 @@ class HandshakeTestActor : public Actor {
           return stop();
         }
         if (!handshake_->is_ready_for_finish()) {
-          finish(Status::Error("Key is not ready.."));
+          finish(td::Status::Error("Key is not ready.."));
           return stop();
         }
-        finish(Status::OK());
+        finish(td::Status::OK());
         return stop();
       }
 
       wait_for_result_ = true;
-      create_actor(
-          "HandshakeActor", std::move(handshake_), std::move(raw_connection_), std::make_unique(), 10.0,
-          PromiseCreator::lambda([self = actor_id(this)](Result> raw_connection) {
-            send_closure(self, &HandshakeTestActor::got_connection, std::move(raw_connection), 1);
-          }),
-          PromiseCreator::lambda([self = actor_id(this)](Result> handshake) {
-            send_closure(self, &HandshakeTestActor::got_handshake, std::move(handshake), 1);
-          }))
+      td::create_actor(
+          "HandshakeActor", std::move(handshake_), std::move(raw_connection_), td::make_unique(),
+          10.0,
+          td::PromiseCreator::lambda(
+              [actor_id = actor_id(this)](td::Result> raw_connection) {
+                td::send_closure(actor_id, &HandshakeTestActor::got_connection, std::move(raw_connection), 1);
+              }),
+          td::PromiseCreator::lambda(
+              [actor_id = actor_id(this)](td::Result> handshake) {
+                td::send_closure(actor_id, &HandshakeTestActor::got_handshake, std::move(handshake), 1);
+              }))
           .release();
       wait_for_raw_connection_ = true;
       wait_for_handshake_ = true;
     }
   }
 
-  void got_connection(Result> r_raw_connection, int32 dummy) {
+  void got_connection(td::Result> r_raw_connection, bool dummy) {
     CHECK(wait_for_raw_connection_);
     wait_for_raw_connection_ = false;
     if (r_raw_connection.is_ok()) {
       raw_connection_ = r_raw_connection.move_as_ok();
-      status_ = Status::OK();
+      status_ = td::Status::OK();
     } else {
       status_ = r_raw_connection.move_as_error();
     }
@@ -246,7 +390,7 @@ class HandshakeTestActor : public Actor {
     loop();
   }
 
-  void got_handshake(Result> r_handshake, int32 dummy) {
+  void got_handshake(td::Result> r_handshake, bool dummy) {
     CHECK(wait_for_handshake_);
     wait_for_handshake_ = false;
     CHECK(r_handshake.is_ok());
@@ -254,23 +398,22 @@ class HandshakeTestActor : public Actor {
     loop();
   }
 
-  void finish(Status status) {
+  void finish(td::Status status) {
     if (!result_) {
       return;
     }
     *result_ = std::move(status);
     result_ = nullptr;
-    Scheduler::instance()->finish();
+    td::Scheduler::instance()->finish();
   }
 };
 
-class Mtproto_handshake : public td::Test {
+class Mtproto_handshake final : public td::Test {
  public:
   using Test::Test;
   bool step() final {
     if (!is_inited_) {
-      sched_.init(0);
-      sched_.create_actor_unsafe(0, "HandshakeTestActor", &result_).release();
+      sched_.create_actor_unsafe(0, "HandshakeTestActor", get_default_dc_id(), &result_).release();
       sched_.start();
       is_inited_ = true;
     }
@@ -283,60 +426,62 @@ class Mtproto_handshake : public td::Test {
     if (result_.is_error()) {
       LOG(ERROR) << result_;
     }
-    ASSERT_TRUE(result_.is_ok());
     return false;
   }
 
  private:
   bool is_inited_ = false;
-  ConcurrentScheduler sched_;
-  Status result_;
+  td::ConcurrentScheduler sched_{0, 0};
+  td::Status result_;
 };
-Mtproto_handshake mtproto_handshake("Mtproto_handshake");
+td::RegisterTest mtproto_handshake("Mtproto_handshake");
 
-class Socks5TestActor : public Actor {
+class Socks5TestActor final : public td::Actor {
  public:
-  void start_up() override {
-    auto promise = PromiseCreator::lambda([actor_id = actor_id(this)](Result res) {
-      send_closure(actor_id, &Socks5TestActor::on_result, std::move(res), false);
-    });
+  void start_up() final {
+    auto promise =
+        td::PromiseCreator::lambda([actor_id = actor_id(this)](td::Result> res) {
+          td::send_closure(actor_id, &Socks5TestActor::on_result, std::move(res), false);
+        });
 
-    class Callback : public Socks5::Callback {
+    class Callback final : public td::TransparentProxy::Callback {
      public:
-      explicit Callback(Promise promise) : promise_(std::move(promise)) {
+      explicit Callback(td::Promise> promise) : promise_(std::move(promise)) {
       }
-      void set_result(Result result) override {
+      void set_result(td::Result> result) final {
         promise_.set_result(std::move(result));
       }
-      void on_connected() override {
+      void on_connected() final {
       }
 
      private:
-      Promise promise_;
+      td::Promise> promise_;
     };
 
-    IPAddress socks5_ip;
+    td::IPAddress socks5_ip;
     socks5_ip.init_ipv4_port("131.191.89.104", 43077).ensure();
-    IPAddress mtproto_ip = get_default_ip_address();
+    td::IPAddress mtproto_ip_address = get_default_ip_address();
 
-    auto r_socket = SocketFd::open(socks5_ip);
-    create_actor("socks5", r_socket.move_as_ok(), mtproto_ip, "", "",
-                         std::make_unique(std::move(promise)), actor_shared())
+    auto r_socket = td::SocketFd::open(socks5_ip);
+    if (r_socket.is_error()) {
+      return promise.set_error(td::Status::Error(PSTRING() << "Failed to open socket: " << r_socket.error()));
+    }
+    td::create_actor("Socks5", r_socket.move_as_ok(), mtproto_ip_address, "", "",
+                                 td::make_unique(std::move(promise)), actor_shared(this))
         .release();
   }
 
  private:
-  void on_result(Result res, bool dummy) {
+  void on_result(td::Result> res, bool dummy) {
     res.ensure();
-    Scheduler::instance()->finish();
+    td::Scheduler::instance()->finish();
   }
 };
 
 TEST(Mtproto, socks5) {
   return;
-  ConcurrentScheduler sched;
   int threads_n = 0;
-  sched.init(threads_n);
+  td::ConcurrentScheduler sched(threads_n, 0);
 
   sched.create_actor_unsafe(0, "Socks5TestActor").release();
   sched.start();
@@ -345,3 +490,251 @@ TEST(Mtproto, socks5) {
   }
   sched.finish();
 }
+
+TEST(Mtproto, notifications) {
+  td::vector pushes = {
+      "eyJwIjoiSkRnQ3NMRWxEaWhyVWRRN1pYM3J1WVU4TlRBMFhMb0N6UWRNdzJ1cWlqMkdRbVR1WXVvYXhUeFJHaG1QQm8yVElYZFBzX2N3b2RIb3lY"
+      "b2drVjM1dVl0UzdWeElNX1FNMDRKMG1mV3ZZWm4zbEtaVlJ0aFVBNGhYUWlaN0pfWDMyZDBLQUlEOWgzRnZwRjNXUFRHQWRaVkdFYzg3bnFPZ3hD"
+      "NUNMRkM2SU9fZmVqcEpaV2RDRlhBWWpwc1k2aktrbVNRdFZ1MzE5ZW04UFVieXZudFpfdTNud2hjQ0czMk96TGp4S1kyS1lzU21JZm1GMzRmTmw1"
+      "QUxaa2JvY2s2cE5rZEdrak9qYmRLckJyU0ZtWU8tQ0FsRE10dEplZFFnY1U5bVJQdU80b1d2NG5sb1VXS19zSlNTaXdIWEZyb1pWTnZTeFJ0Z1dN"
+      "ZyJ9",
+      "eyJwIjoiSkRnQ3NMRWxEaWlZby1GRWJndk9WaTFkUFdPVmZndzBBWHYwTWNzWDFhWEtNZC03T1Q2WWNfT0taRURHZDJsZ0h0WkhMSllyVG50RE95"
+      "TkY1aXJRQlZ4UUFLQlRBekhPTGZIS3BhQXdoaWd5b3NQd0piWnJVV2xRWmh4eEozUFUzZjBNRTEwX0xNT0pFN0xsVUFaY2dabUNaX2V1QmNPZWNK"
+      "VERxRkpIRGZjN2pBOWNrcFkyNmJRT2dPUEhCeHlEMUVrNVdQcFpLTnlBODVuYzQ1eHFPdERqcU5aVmFLU3pKb2VIcXBQMnJqR29kN2M5YkxsdGd5"
+      "Q0NGd2NBU3dJeDc3QWNWVXY1UnVZIn0"};
+  td::string key =
+      "uBa5yu01a-nJJeqsR3yeqMs6fJLYXjecYzFcvS6jIwS3nefBIr95LWrTm-IbRBNDLrkISz1Sv0KYpDzhU8WFRk1D0V_"
+      "qyO7XsbDPyrYxRBpGxofJUINSjb1uCxoSdoh1_F0UXEA2fWWKKVxL0DKUQssZfbVj3AbRglsWpH-jDK1oc6eBydRiS3i4j-"
+      "H0yJkEMoKRgaF9NaYI4u26oIQ-Ez46kTVU-R7e3acdofOJKm7HIKan_5ZMg82Dvec2M6vc_"
+      "I54Vs28iBx8IbBO1y5z9WSScgW3JCvFFKP2MXIu7Jow5-cpUx6jXdzwRUb9RDApwAFKi45zpv8eb3uPCDAmIQ";
+  td::vector decrypted_payloads = {
+      "eyJsb2Nfa2V5IjoiTUVTU0FHRV9URVhUIiwibG9jX2FyZ3MiOlsiQXJzZW55IFNtaXJub3YiLCJhYmNkZWZnIl0sImN1c3RvbSI6eyJtc2dfaWQi"
+      "OiI1OTAwNDciLCJmcm9tX2lkIjoiNjI4MTQifSwiYmFkZ2UiOiI0MDkifQ",
+      "eyJsb2Nfa2V5IjoiIiwibG9jX2FyZ3MiOltdLCJjdXN0b20iOnsiY2hhbm5lbF9pZCI6IjExNzY4OTU0OTciLCJtYXhfaWQiOiIxMzU5In0sImJh"
+      "ZGdlIjoiMCJ9"};
+  key = td::base64url_decode(key).move_as_ok();
+
+  for (size_t i = 0; i < pushes.size(); i++) {
+    auto push = td::base64url_decode(pushes[i]).move_as_ok();
+    auto decrypted_payload = td::base64url_decode(decrypted_payloads[i]).move_as_ok();
+
+    auto key_id = td::mtproto::DhHandshake::calc_key_id(key);
+    ASSERT_EQ(key_id, td::NotificationManager::get_push_receiver_id(push).ok());
+    ASSERT_EQ(decrypted_payload, td::NotificationManager::decrypt_push(key_id, key, push).ok());
+  }
+}
+
+class FastPingTestActor final : public td::Actor {
+ public:
+  explicit FastPingTestActor(td::Status *result) : result_(result) {
+  }
+
+ private:
+  td::Status *result_;
+  td::unique_ptr connection_;
+  td::unique_ptr handshake_;
+  td::ActorOwn<> fast_ping_;
+  int iteration_{0};
+
+  void start_up() final {
+    // Run handshake to create key and salt
+    auto ip_address = get_default_ip_address();
+    auto r_socket = td::SocketFd::open(ip_address);
+    if (r_socket.is_error()) {
+      *result_ = td::Status::Error(PSTRING() << "Failed to open socket: " << r_socket.error());
+      return stop();
+    }
+
+    auto raw_connection = td::mtproto::RawConnection::create(
+        ip_address, td::BufferedFd(r_socket.move_as_ok()),
+        td::mtproto::TransportType{td::mtproto::TransportType::Tcp, 0, td::mtproto::ProxySecret()}, nullptr);
+    auto handshake = td::make_unique(get_default_dc_id(), 60 * 100 /*temp*/);
+    td::create_actor(
+        "HandshakeActor", std::move(handshake), std::move(raw_connection), td::make_unique(), 10.0,
+        td::PromiseCreator::lambda(
+            [actor_id = actor_id(this)](td::Result> raw_connection) {
+              td::send_closure(actor_id, &FastPingTestActor::got_connection, std::move(raw_connection), 1);
+            }),
+        td::PromiseCreator::lambda(
+            [actor_id = actor_id(this)](td::Result> handshake) {
+              td::send_closure(actor_id, &FastPingTestActor::got_handshake, std::move(handshake), 1);
+            }))
+        .release();
+  }
+
+  void got_connection(td::Result> r_raw_connection, bool dummy) {
+    if (r_raw_connection.is_error()) {
+      *result_ = r_raw_connection.move_as_error();
+      LOG(INFO) << "Receive " << *result_ << " instead of a connection";
+      return stop();
+    }
+    connection_ = r_raw_connection.move_as_ok();
+    loop();
+  }
+
+  void got_handshake(td::Result> r_handshake, bool dummy) {
+    if (r_handshake.is_error()) {
+      *result_ = r_handshake.move_as_error();
+      LOG(INFO) << "Receive " << *result_ << " instead of a handshake";
+      return stop();
+    }
+    handshake_ = r_handshake.move_as_ok();
+    loop();
+  }
+
+  void got_raw_connection(td::Result> r_connection) {
+    if (r_connection.is_error()) {
+      *result_ = r_connection.move_as_error();
+      LOG(INFO) << "Receive " << *result_ << " instead of a handshake";
+      return stop();
+    }
+    connection_ = r_connection.move_as_ok();
+    LOG(INFO) << "RTT: " << connection_->extra().rtt;
+    connection_->extra().rtt = 0;
+    loop();
+  }
+
+  void loop() final {
+    if (handshake_ && connection_) {
+      LOG(INFO) << "Iteration " << iteration_;
+      if (iteration_ == 6) {
+        return stop();
+      }
+      td::unique_ptr auth_data;
+      if (iteration_ % 2 == 0) {
+        auth_data = td::make_unique();
+        auth_data->set_tmp_auth_key(handshake_->get_auth_key());
+        auth_data->set_server_time_difference(handshake_->get_server_time_diff());
+        auth_data->set_server_salt(handshake_->get_server_salt(), td::Time::now());
+        auth_data->set_future_salts({td::mtproto::ServerSalt{0u, 1e20, 1e30}}, td::Time::now());
+        auth_data->set_use_pfs(true);
+        td::uint64 session_id = 0;
+        do {
+          td::Random::secure_bytes(reinterpret_cast(&session_id), sizeof(session_id));
+        } while (session_id == 0);
+        auth_data->set_session_id(session_id);
+      }
+      iteration_++;
+      fast_ping_ = create_ping_actor(
+          td::Slice(), std::move(connection_), std::move(auth_data),
+          td::PromiseCreator::lambda(
+              [actor_id = actor_id(this)](td::Result> r_raw_connection) {
+                td::send_closure(actor_id, &FastPingTestActor::got_raw_connection, std::move(r_raw_connection));
+              }),
+          td::ActorShared<>());
+    }
+  }
+
+  void tear_down() final {
+    td::Scheduler::instance()->finish();
+  }
+};
+
+class Mtproto_FastPing final : public td::Test {
+ public:
+  using Test::Test;
+  bool step() final {
+    if (!is_inited_) {
+      sched_.create_actor_unsafe(0, "FastPingTestActor", &result_).release();
+      sched_.start();
+      is_inited_ = true;
+    }
+
+    bool ret = sched_.run_main(10);
+    if (ret) {
+      return true;
+    }
+    sched_.finish();
+    if (result_.is_error()) {
+      LOG(ERROR) << result_;
+    }
+    return false;
+  }
+
+ private:
+  bool is_inited_ = false;
+  td::ConcurrentScheduler sched_{0, 0};
+  td::Status result_;
+};
+td::RegisterTest mtproto_fastping("Mtproto_FastPing");
+
+TEST(Mtproto, Grease) {
+  td::string s(10000, '0');
+  td::mtproto::Grease::init(s);
+  for (auto c : s) {
+    CHECK((c & 0xF) == 0xA);
+  }
+  for (size_t i = 1; i < s.size(); i += 2) {
+    CHECK(s[i] != s[i - 1]);
+  }
+}
+
+TEST(Mtproto, TlsTransport) {
+  int threads_n = 1;
+  td::ConcurrentScheduler sched(threads_n, 0);
+  {
+    auto guard = sched.get_main_guard();
+    class RunTest final : public td::Actor {
+      void start_up() final {
+        class Callback final : public td::TransparentProxy::Callback {
+         public:
+          void set_result(td::Result> result) final {
+            if (result.is_ok()) {
+              LOG(ERROR) << "Unexpectedly succeeded to connect to MTProto proxy";
+            } else if (result.error().message() != "Response hash mismatch") {
+              LOG(ERROR) << "Receive unexpected result " << result.error();
+            }
+            td::Scheduler::instance()->finish();
+          }
+          void on_connected() final {
+          }
+        };
+
+        const td::string domain = "www.google.com";
+        td::IPAddress ip_address;
+        auto resolve_status = ip_address.init_host_port(domain, 443);
+        if (resolve_status.is_error()) {
+          LOG(ERROR) << resolve_status;
+          td::Scheduler::instance()->finish();
+          return;
+        }
+        auto r_socket = td::SocketFd::open(ip_address);
+        if (r_socket.is_error()) {
+          LOG(ERROR) << "Failed to open socket: " << r_socket.error();
+          td::Scheduler::instance()->finish();
+          return;
+        }
+        td::create_actor("TlsInit", r_socket.move_as_ok(), domain, "0123456789secret",
+                                               td::make_unique(), td::ActorShared<>(),
+                                               td::Clocks::system() - td::Time::now())
+            .release();
+      }
+    };
+    td::create_actor("RunTest").release();
+  }
+
+  sched.start();
+  while (sched.run_main(10)) {
+    // empty
+  }
+  sched.finish();
+}
+
+TEST(Mtproto, RSA) {
+  auto pem = td::Slice(
+      "-----BEGIN RSA PUBLIC KEY-----\n"
+      "MIIBCgKCAQEAr4v4wxMDXIaMOh8bayF/NyoYdpcysn5EbjTIOZC0RkgzsRj3SGlu\n"
+      "52QSz+ysO41dQAjpFLgxPVJoOlxXokaOq827IfW0bGCm0doT5hxtedu9UCQKbE8j\n"
+      "lDOk+kWMXHPZFJKWRgKgTu9hcB3y3Vk+JFfLpq3d5ZB48B4bcwrRQnzkx5GhWOFX\n"
+      "x73ZgjO93eoQ2b/lDyXxK4B4IS+hZhjzezPZTI5upTRbs5ljlApsddsHrKk6jJNj\n"
+      "8Ygs/ps8e6ct82jLXbnndC9s8HjEvDvBPH9IPjv5JUlmHMBFZ5vFQIfbpo0u0+1P\n"
+      "n6bkEi5o7/ifoyVv2pAZTRwppTz0EuXD8QIDAQAB\n"
+      "-----END RSA PUBLIC KEY-----");
+  auto rsa = td::mtproto::RSA::from_pem_public_key(pem).move_as_ok();
+  ASSERT_EQ(-7596991558377038078, rsa.get_fingerprint());
+  ASSERT_EQ(256u, rsa.size());
+
+  td::string to(256, '\0');
+  rsa.encrypt(pem.substr(0, 256), to);
+  ASSERT_EQ("U2nJEtB2AgpHrm3HB0yhpTQgb0wbesi9Pv/W1v/vULU=", td::base64_encode(td::sha256(to)));
+}
diff --git a/protocols/Telegram/tdlib/td/test/online.cpp b/protocols/Telegram/tdlib/td/test/online.cpp
new file mode 100644
index 0000000000..6bcdcec833
--- /dev/null
+++ b/protocols/Telegram/tdlib/td/test/online.cpp
@@ -0,0 +1,632 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/telegram/ClientActor.h"
+#include "td/telegram/Log.h"
+#include "td/telegram/td_api_json.h"
+#include "td/telegram/TdCallback.h"
+
+#include "td/actor/actor.h"
+#include "td/actor/ConcurrentScheduler.h"
+#include "td/actor/MultiPromise.h"
+#include "td/actor/PromiseFuture.h"
+
+#include "td/utils/crypto.h"
+#include "td/utils/FileLog.h"
+#include "td/utils/filesystem.h"
+#include "td/utils/misc.h"
+#include "td/utils/OptionParser.h"
+#include "td/utils/port/path.h"
+#include "td/utils/port/signals.h"
+#include "td/utils/Promise.h"
+#include "td/utils/Random.h"
+
+#include 
+#include 
+#include 
+
+namespace td {
+
+template 
+static void check_td_error(T &result) {
+  LOG_CHECK(result->get_id() != td::td_api::error::ID) << to_string(result);
+}
+
+class TestClient : public Actor {
+ public:
+  explicit TestClient(td::string name) : name_(std::move(name)) {
+  }
+  struct Update {
+    td::uint64 id;
+    td::tl_object_ptr object;
+    Update(td::uint64 id, td::tl_object_ptr object) : id(id), object(std::move(object)) {
+    }
+  };
+  class Listener {
+   public:
+    Listener() = default;
+    Listener(const Listener &) = delete;
+    Listener &operator=(const Listener &) = delete;
+    Listener(Listener &&) = delete;
+    Listener &operator=(Listener &&) = delete;
+    virtual ~Listener() = default;
+    virtual void start_listen(TestClient *client) {
+    }
+    virtual void stop_listen() {
+    }
+    virtual void on_update(std::shared_ptr update) = 0;
+  };
+  struct RemoveListener {
+    void operator()(Listener *listener) {
+      send_closure(self, &TestClient::remove_listener, listener);
+    }
+    ActorId self;
+  };
+  using ListenerToken = std::unique_ptr;
+  void close(td::Promise<> close_promise) {
+    close_promise_ = std::move(close_promise);
+    td_client_.reset();
+  }
+
+  td::unique_ptr make_td_callback() {
+    class TdCallbackImpl : public td::TdCallback {
+     public:
+      explicit TdCallbackImpl(td::ActorId client) : client_(client) {
+      }
+      void on_result(td::uint64 id, td::tl_object_ptr result) override {
+        send_closure(client_, &TestClient::on_result, id, std::move(result));
+      }
+      void on_error(td::uint64 id, td::tl_object_ptr error) override {
+        send_closure(client_, &TestClient::on_error, id, std::move(error));
+      }
+      TdCallbackImpl(const TdCallbackImpl &) = delete;
+      TdCallbackImpl &operator=(const TdCallbackImpl &) = delete;
+      TdCallbackImpl(TdCallbackImpl &&) = delete;
+      TdCallbackImpl &operator=(TdCallbackImpl &&) = delete;
+      ~TdCallbackImpl() override {
+        send_closure(client_, &TestClient::on_closed);
+      }
+
+     private:
+      td::ActorId client_;
+    };
+    return td::make_unique(actor_id(this));
+  }
+
+  void add_listener(td::unique_ptr listener) {
+    auto *ptr = listener.get();
+    listeners_.push_back(std::move(listener));
+    ptr->start_listen(this);
+  }
+  void remove_listener(Listener *listener) {
+    pending_remove_.push_back(listener);
+  }
+  void do_pending_remove_listeners() {
+    for (auto listener : pending_remove_) {
+      do_remove_listener(listener);
+    }
+    pending_remove_.clear();
+  }
+  void do_remove_listener(Listener *listener) {
+    for (size_t i = 0; i < listeners_.size(); i++) {
+      if (listeners_[i].get() == listener) {
+        listener->stop_listen();
+        listeners_.erase(listeners_.begin() + i);
+        break;
+      }
+    }
+  }
+
+  void on_result(td::uint64 id, td::tl_object_ptr result) {
+    on_update(std::make_shared(id, std::move(result)));
+  }
+  void on_error(td::uint64 id, td::tl_object_ptr error) {
+    on_update(std::make_shared(id, std::move(error)));
+  }
+  void on_update(std::shared_ptr update) {
+    for (auto &listener : listeners_) {
+      listener->on_update(update);
+    }
+    do_pending_remove_listeners();
+  }
+
+  void on_closed() {
+    stop();
+  }
+
+  void start_up() override {
+    auto old_context = set_context(std::make_shared());
+    set_tag(name_);
+    LOG(INFO) << "START UP!";
+
+    td_client_ = td::create_actor("Td-proxy", make_td_callback());
+  }
+
+  td::ActorOwn td_client_;
+
+  td::string name_;
+
+ private:
+  td::vector> listeners_;
+  td::vector pending_remove_;
+
+  td::Promise<> close_promise_;
+};
+
+class Task : public TestClient::Listener {
+ public:
+  void on_update(std::shared_ptr update) override {
+    auto it = sent_queries_.find(update->id);
+    if (it != sent_queries_.end()) {
+      it->second.set_value(std::move(update->object));
+      sent_queries_.erase(it);
+    }
+    process_update(update);
+  }
+  void start_listen(TestClient *client) override {
+    client_ = client;
+    start_up();
+  }
+  virtual void process_update(std::shared_ptr update) {
+  }
+
+  template 
+  void send_query(td::tl_object_ptr function, CallbackT callback) {
+    auto id = current_query_id_++;
+
+    using ResultT = typename FunctionT::ReturnType;
+    sent_queries_[id] =
+        [callback = Promise(std::move(callback))](Result> r_obj) mutable {
+          TRY_RESULT_PROMISE(callback, obj, std::move(r_obj));
+          if (obj->get_id() == td::td_api::error::ID) {
+            auto err = move_tl_object_as(std::move(obj));
+            callback.set_error(Status::Error(err->code_, err->message_));
+            return;
+          }
+          callback.set_value(move_tl_object_as(std::move(obj)));
+        };
+    send_closure(client_->td_client_, &td::ClientActor::request, id, std::move(function));
+  }
+
+ protected:
+  std::map>> sent_queries_;
+  TestClient *client_ = nullptr;
+  td::uint64 current_query_id_ = 1;
+
+  virtual void start_up() {
+  }
+  void stop() {
+    client_->remove_listener(this);
+    client_ = nullptr;
+  }
+  bool is_alive() const {
+    return client_ != nullptr;
+  }
+};
+
+class InitTask : public Task {
+ public:
+  struct Options {
+    string name;
+    int32 api_id;
+    string api_hash;
+  };
+  InitTask(Options options, td::Promise<> promise) : options_(std::move(options)), promise_(std::move(promise)) {
+  }
+
+ private:
+  Options options_;
+  td::Promise<> promise_;
+  bool start_flag_{false};
+
+  void start_up() override {
+    send_query(td::make_tl_object(),
+               [this](auto res) { this->process_authorization_state(res.move_as_ok()); });
+  }
+  void process_authorization_state(td::tl_object_ptr authorization_state) {
+    start_flag_ = true;
+    td::tl_object_ptr function;
+    switch (authorization_state->get_id()) {
+      case td::td_api::authorizationStateReady::ID:
+        promise_.set_value({});
+        stop();
+        break;
+      case td::td_api::authorizationStateWaitTdlibParameters::ID: {
+        auto request = td::td_api::make_object();
+        request->use_test_dc_ = true;
+        request->database_directory_ = options_.name + TD_DIR_SLASH;
+        request->use_message_database_ = true;
+        request->use_secret_chats_ = true;
+        request->api_id_ = options_.api_id;
+        request->api_hash_ = options_.api_hash;
+        request->system_language_code_ = "en";
+        request->device_model_ = "Desktop";
+        request->application_version_ = "tdclient-test";
+        request->enable_storage_optimizer_ = true;
+        send(std::move(request));
+        break;
+      }
+      default:
+        LOG(ERROR) << "???";
+        promise_.set_error(
+            Status::Error(PSLICE() << "Unexpected authorization state " << to_string(authorization_state)));
+        stop();
+        break;
+    }
+  }
+  template 
+  void send(T &&query) {
+    send_query(std::move(query), [this](auto res) {
+      if (is_alive()) {
+        res.ensure();
+      }
+    });
+  }
+  void process_update(std::shared_ptr update) override {
+    if (!start_flag_) {
+      return;
+    }
+    if (!update->object) {
+      return;
+    }
+    if (update->object->get_id() == td::td_api::updateAuthorizationState::ID) {
+      auto update_authorization_state = td::move_tl_object_as(update->object);
+      process_authorization_state(std::move(update_authorization_state->authorization_state_));
+    }
+  }
+};
+
+class GetMe : public Task {
+ public:
+  struct Result {
+    int64 user_id;
+    int64 chat_id;
+  };
+  explicit GetMe(Promise promise) : promise_(std::move(promise)) {
+  }
+  void start_up() override {
+    send_query(td::make_tl_object(), [this](auto res) { with_user_id(res.move_as_ok()->id_); });
+  }
+
+ private:
+  Promise promise_;
+  Result result_;
+
+  void with_user_id(int64 user_id) {
+    result_.user_id = user_id;
+    send_query(td::make_tl_object(user_id, false),
+               [this](auto res) { with_chat_id(res.move_as_ok()->id_); });
+  }
+
+  void with_chat_id(int64 chat_id) {
+    result_.chat_id = chat_id;
+    promise_.set_value(std::move(result_));
+    stop();
+  }
+};
+
+class UploadFile : public Task {
+ public:
+  struct Result {
+    std::string content;
+    std::string remote_id;
+  };
+  UploadFile(std::string dir, std::string content, int64 chat_id, Promise promise)
+      : dir_(std::move(dir)), content_(std::move(content)), chat_id_(std::move(chat_id)), promise_(std::move(promise)) {
+  }
+  void start_up() override {
+    auto hash = hex_encode(sha256(content_)).substr(0, 10);
+    content_path_ = dir_ + TD_DIR_SLASH + hash + ".data";
+    id_path_ = dir_ + TD_DIR_SLASH + hash + ".id";
+
+    auto r_id = read_file(id_path_);
+    if (r_id.is_ok() && r_id.ok().size() > 10) {
+      auto id = r_id.move_as_ok();
+      LOG(ERROR) << "Got file from cache";
+      Result res;
+      res.content = std::move(content_);
+      res.remote_id = id.as_slice().str();
+      promise_.set_value(std::move(res));
+      stop();
+      return;
+    }
+
+    write_file(content_path_, content_).ensure();
+
+    send_query(td::make_tl_object(
+                   chat_id_, 0, 0, nullptr, nullptr,
+                   td::make_tl_object(
+                       td::make_tl_object(content_path_), nullptr, true,
+                       td::make_tl_object("tag", td::Auto()))),
+               [this](auto res) { with_message(res.move_as_ok()); });
+  }
+
+ private:
+  std::string dir_;
+  std::string content_path_;
+  std::string id_path_;
+  std::string content_;
+  int64 chat_id_;
+  Promise promise_;
+  int64 file_id_{0};
+
+  void with_message(td::tl_object_ptr message) {
+    CHECK(message->content_->get_id() == td::td_api::messageDocument::ID);
+    auto messageDocument = td::move_tl_object_as(message->content_);
+    on_file(*messageDocument->document_->document_, true);
+  }
+
+  void on_file(const td_api::file &file, bool force = false) {
+    if (force) {
+      file_id_ = file.id_;
+    }
+    if (file.id_ != file_id_) {
+      return;
+    }
+    if (file.remote_->is_uploading_completed_) {
+      Result res;
+      res.content = std::move(content_);
+      res.remote_id = file.remote_->id_;
+
+      unlink(content_path_).ignore();
+      atomic_write_file(id_path_, res.remote_id).ignore();
+
+      promise_.set_value(std::move(res));
+      stop();
+    }
+  }
+
+  void process_update(std::shared_ptr update) override {
+    if (!update->object) {
+      return;
+    }
+    if (update->object->get_id() == td::td_api::updateFile::ID) {
+      auto updateFile = td::move_tl_object_as(update->object);
+      on_file(*updateFile->file_);
+    }
+  }
+};
+
+class TestDownloadFile : public Task {
+ public:
+  TestDownloadFile(std::string remote_id, std::string content, Promise promise)
+      : remote_id_(std::move(remote_id)), content_(std::move(content)), promise_(std::move(promise)) {
+  }
+  void start_up() override {
+    send_query(td::make_tl_object(remote_id_, nullptr),
+               [this](auto res) { start_file(*res.ok()); });
+  }
+
+ private:
+  std::string remote_id_;
+  std::string content_;
+  Promise promise_;
+  struct Range {
+    size_t begin;
+    size_t end;
+  };
+  int32 file_id_{0};
+  std::vector ranges_;
+
+  void start_file(const td_api::file &file) {
+    LOG(ERROR) << "Start";
+    file_id_ = file.id_;
+    //    CHECK(!file.local_->is_downloading_active_);
+    //    CHECK(!file.local_->is_downloading_completed_);
+    //    CHECK(file.local_->download_offset_ == 0);
+    if (!file.local_->path_.empty()) {
+      unlink(file.local_->path_).ignore();
+    }
+
+    auto size = narrow_cast(file.size_);
+    Random::Xorshift128plus rnd(123);
+
+    size_t begin = 0;
+
+    while (begin + 128u < size) {
+      auto chunk_size = rnd.fast(128, 3096);
+      auto end = begin + chunk_size;
+      if (end > size) {
+        end = size;
+      }
+
+      ranges_.push_back({begin, end});
+      begin = end;
+    }
+
+    random_shuffle(as_mutable_span(ranges_), rnd);
+    start_chunk();
+  }
+
+  void got_chunk(const td_api::file &file) {
+    LOG(ERROR) << "Got chunk";
+    auto range = ranges_.back();
+    std::string got_chunk(range.end - range.begin, '\0');
+    FileFd::open(file.local_->path_, FileFd::Flags::Read).move_as_ok().pread(got_chunk, range.begin).ensure();
+    CHECK(got_chunk == as_slice(content_).substr(range.begin, range.end - range.begin));
+    ranges_.pop_back();
+    if (ranges_.empty()) {
+      promise_.set_value(Unit{});
+      return stop();
+    }
+    start_chunk();
+  }
+
+  void start_chunk() {
+    send_query(td::make_tl_object(
+                   file_id_, 1, static_cast(ranges_.back().begin),
+                   static_cast(ranges_.back().end - ranges_.back().begin), true),
+               [this](auto res) { got_chunk(*res.ok()); });
+  }
+};
+
+static std::string gen_readable_file(size_t block_size, size_t block_count) {
+  std::string content;
+  for (size_t block_id = 0; block_id < block_count; block_id++) {
+    std::string block;
+    for (size_t line = 0; block.size() < block_size; line++) {
+      block += PSTRING() << "\nblock=" << block_id << ", line=" << line;
+    }
+    block.resize(block_size);
+    content += block;
+  }
+  return content;
+}
+
+class TestTd : public Actor {
+ public:
+  struct Options {
+    std::string alice_dir = "alice";
+    std::string bob_dir = "bob";
+    int32 api_id{0};
+    string api_hash;
+  };
+
+  explicit TestTd(Options options) : options_(std::move(options)) {
+  }
+
+ private:
+  Options options_;
+  ActorOwn alice_;
+  GetMe::Result alice_id_;
+  std::string alice_cache_dir_;
+  ActorOwn bob_;
+
+  void start_up() override {
+    alice_ = create_actor("Alice", "Alice");
+    bob_ = create_actor("Bob", "Bob");
+
+    MultiPromiseActorSafe mp("init");
+    mp.add_promise(promise_send_closure(actor_id(this), &TestTd::check_init));
+
+    InitTask::Options options;
+    options.api_id = options_.api_id;
+    options.api_hash = options_.api_hash;
+
+    options.name = options_.alice_dir;
+    td::send_closure(alice_, &TestClient::add_listener, td::make_unique(options, mp.get_promise()));
+    options.name = options_.bob_dir;
+    td::send_closure(bob_, &TestClient::add_listener, td::make_unique(options, mp.get_promise()));
+  }
+
+  void check_init(Result res) {
+    LOG_IF(FATAL, res.is_error()) << res.error();
+    alice_cache_dir_ = options_.alice_dir + TD_DIR_SLASH + "cache";
+    mkdir(alice_cache_dir_).ignore();
+
+    td::send_closure(alice_, &TestClient::add_listener,
+                     td::make_unique(promise_send_closure(actor_id(this), &TestTd::with_alice_id)));
+
+    //close();
+  }
+
+  void with_alice_id(Result alice_id) {
+    alice_id_ = alice_id.move_as_ok();
+    LOG(ERROR) << "Alice user_id=" << alice_id_.user_id << ", chat_id=" << alice_id_.chat_id;
+    auto content = gen_readable_file(65536, 20);
+    send_closure(alice_, &TestClient::add_listener,
+                 td::make_unique(alice_cache_dir_, std::move(content), alice_id_.chat_id,
+                                             promise_send_closure(actor_id(this), &TestTd::with_file)));
+  }
+  void with_file(Result r_result) {
+    auto result = r_result.move_as_ok();
+    send_closure(
+        alice_, &TestClient::add_listener,
+        td::make_unique(result.remote_id, std::move(result.content),
+                                          promise_send_closure(actor_id(this), &TestTd::after_test_download_file)));
+  }
+  void after_test_download_file(Result) {
+    close();
+  }
+
+  void close() {
+    MultiPromiseActorSafe mp("close");
+    mp.add_promise(promise_send_closure(actor_id(this), &TestTd::check_close));
+    td::send_closure(alice_, &TestClient::close, mp.get_promise());
+    td::send_closure(bob_, &TestClient::close, mp.get_promise());
+  }
+
+  void check_close(Result res) {
+    Scheduler::instance()->finish();
+    stop();
+  }
+};
+
+static void fail_signal(int sig) {
+  signal_safe_write_signal_number(sig);
+  while (true) {
+    // spin forever to allow debugger to attach
+  }
+}
+
+static void on_fatal_error(const char *error) {
+  std::cerr << "Fatal error: " << error << std::endl;
+}
+int main(int argc, char **argv) {
+  ignore_signal(SignalType::HangUp).ensure();
+  ignore_signal(SignalType::Pipe).ensure();
+  set_signal_handler(SignalType::Error, fail_signal).ensure();
+  set_signal_handler(SignalType::Abort, fail_signal).ensure();
+  Log::set_fatal_error_callback(on_fatal_error);
+  init_openssl_threads();
+
+  TestTd::Options test_options;
+
+  test_options.api_id = [](auto x) -> int32 {
+    if (x) {
+      return to_integer(Slice(x));
+    }
+    return 0;
+  }(std::getenv("TD_API_ID"));
+  test_options.api_hash = [](auto x) -> std::string {
+    if (x) {
+      return x;
+    }
+    return std::string();
+  }(std::getenv("TD_API_HASH"));
+
+  int new_verbosity_level = VERBOSITY_NAME(INFO);
+
+  OptionParser options;
+  options.set_description("TDLib experimental tester");
+  options.add_option('v', "verbosity", "Set verbosity level", [&](Slice level) {
+    int new_verbosity = 1;
+    while (begins_with(level, "v")) {
+      new_verbosity++;
+      level.remove_prefix(1);
+    }
+    if (!level.empty()) {
+      new_verbosity += to_integer(level) - (new_verbosity == 1);
+    }
+    new_verbosity_level = VERBOSITY_NAME(FATAL) + new_verbosity;
+  });
+  options.add_check([&] {
+    if (test_options.api_id == 0 || test_options.api_hash.empty()) {
+      return Status::Error("You must provide valid api-id and api-hash obtained at https://my.telegram.org");
+    }
+    return Status::OK();
+  });
+  auto r_non_options = options.run(argc, argv, 0);
+  if (r_non_options.is_error()) {
+    LOG(PLAIN) << argv[0] << ": " << r_non_options.error().message();
+    LOG(PLAIN) << options;
+    return 1;
+  }
+  SET_VERBOSITY_LEVEL(new_verbosity_level);
+
+  td::ConcurrentScheduler sched(4, 0);
+  sched.create_actor_unsafe(0, "TestTd", std::move(test_options)).release();
+  sched.start();
+  while (sched.run_main(10)) {
+  }
+  sched.finish();
+  return 0;
+}
+}  // namespace td
+
+int main(int argc, char **argv) {
+  return td::main(argc, argv);
+}
diff --git a/protocols/Telegram/tdlib/td/test/poll.cpp b/protocols/Telegram/tdlib/td/test/poll.cpp
new file mode 100644
index 0000000000..4c8012e441
--- /dev/null
+++ b/protocols/Telegram/tdlib/td/test/poll.cpp
@@ -0,0 +1,58 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/telegram/PollManager.h"
+
+#include "td/utils/logging.h"
+#include "td/utils/tests.h"
+
+static void check_vote_percentage(const std::vector &voter_counts, td::int32 total_count,
+                                  const std::vector &expected) {
+  auto result = td::PollManager::get_vote_percentage(voter_counts, total_count);
+  if (result != expected) {
+    LOG(FATAL) << "Have " << voter_counts << " and " << total_count << ", but received " << result << " instead of "
+               << expected;
+  }
+}
+
+TEST(Poll, get_vote_percentage) {
+  check_vote_percentage({1}, 1, {100});
+  check_vote_percentage({999}, 999, {100});
+  check_vote_percentage({0}, 0, {0});
+  check_vote_percentage({2, 1}, 3, {67, 33});
+  check_vote_percentage({4, 1, 1}, 6, {66, 17, 17});
+  check_vote_percentage({100, 100}, 200, {50, 50});
+  check_vote_percentage({101, 99}, 200, {50, 50});
+  check_vote_percentage({102, 98}, 200, {51, 49});
+  check_vote_percentage({198, 2}, 200, {99, 1});
+  check_vote_percentage({199, 1}, 200, {99, 1});
+  check_vote_percentage({200}, 200, {100});
+  check_vote_percentage({0, 999}, 999, {0, 100});
+  check_vote_percentage({999, 999}, 999, {100, 100});
+  check_vote_percentage({499, 599}, 999, {50, 60});
+  check_vote_percentage({1, 1}, 2, {50, 50});
+  check_vote_percentage({1, 1, 1}, 3, {33, 33, 33});
+  check_vote_percentage({1, 1, 1, 1}, 4, {25, 25, 25, 25});
+  check_vote_percentage({1, 1, 1, 1, 1}, 5, {20, 20, 20, 20, 20});
+  check_vote_percentage({1, 1, 1, 1, 1, 1}, 6, {16, 16, 16, 16, 16, 16});
+  check_vote_percentage({1, 1, 1, 1, 1, 1, 1}, 7, {14, 14, 14, 14, 14, 14, 14});
+  check_vote_percentage({1, 1, 1, 1, 1, 1, 1, 1}, 8, {12, 12, 12, 12, 12, 12, 12, 12});
+  check_vote_percentage({1, 1, 1, 1, 1, 1, 1, 1, 1}, 9, {11, 11, 11, 11, 11, 11, 11, 11, 11});
+  check_vote_percentage({1, 1, 1, 1, 1, 1, 2}, 8, {12, 12, 12, 12, 12, 12, 25});
+  check_vote_percentage({1, 1, 1, 2, 2, 2, 3}, 12, {8, 8, 8, 17, 17, 17, 25});
+  check_vote_percentage({0, 1, 1, 1, 2, 2, 2, 3}, 12, {0, 8, 8, 8, 17, 17, 17, 25});
+  check_vote_percentage({1, 1, 1, 0}, 3, {33, 33, 33, 0});
+  check_vote_percentage({0, 1, 1, 1}, 3, {0, 33, 33, 33});
+  check_vote_percentage({9949, 9950, 9999}, 10000, {99, 100, 100});
+  check_vote_percentage({1234, 2345, 3456, 2841}, 9876,
+                        {12 /* 12.49 */, 24 /* 23.74 */, 35 /* 34.99 */, 29 /* 28.76 */});
+  check_vote_percentage({1234, 2301, 3500, 2841}, 9876,
+                        {12 /* 12.49 */, 23 /* 23.29 */, 35 /* 35.43 */, 29 /* 28.76 */});
+  check_vote_percentage({200, 200, 200, 270, 270, 60}, 1200, {17, 17, 17, 22, 22, 5});
+  check_vote_percentage({200, 200, 200, 300, 240, 60}, 1200, {16, 16, 16, 25, 20, 5});
+  check_vote_percentage({200, 200, 200, 250, 250, 20}, 1120, {18, 18, 18, 22, 22, 2});
+  check_vote_percentage({200, 200, 200, 250, 250, 40}, 1140, {17, 17, 17, 22, 22, 4});
+}
diff --git a/protocols/Telegram/tdlib/td/test/secret.cpp b/protocols/Telegram/tdlib/td/test/secret.cpp
index 2abed9787b..2c79b7e2d8 100644
--- a/protocols/Telegram/tdlib/td/test/secret.cpp
+++ b/protocols/Telegram/tdlib/td/test/secret.cpp
@@ -1,36 +1,48 @@
 //
-// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022
 //
 // Distributed under the Boost Software License, Version 1.0. (See accompanying
 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
 //
-#include "td/actor/PromiseFuture.h"
-
-#include "td/db/binlog/detail/BinlogEventsProcessor.h"
-
-#include "td/mtproto/crypto.h"
-#include "td/mtproto/utils.h"
-
+#include "td/telegram/EncryptedFile.h"
+#include "td/telegram/FolderId.h"
 #include "td/telegram/Global.h"
+#include "td/telegram/logevent/LogEvent.h"
 #include "td/telegram/MessageId.h"
+#include "td/telegram/secret_api.h"
 #include "td/telegram/SecretChatActor.h"
 #include "td/telegram/SecretChatId.h"
-
-#include "td/telegram/secret_api.h"
 #include "td/telegram/telegram_api.h"
+#include "td/telegram/UserId.h"
+
+#include "td/db/binlog/BinlogInterface.h"
+#include "td/db/binlog/detail/BinlogEventsProcessor.h"
+#include "td/db/BinlogKeyValue.h"
+#include "td/db/DbKey.h"
+
+#include "td/mtproto/DhCallback.h"
+#include "td/mtproto/utils.h"
 
 #include "td/tl/tl_object_parse.h"
 #include "td/tl/tl_object_store.h"
 
+#include "td/actor/actor.h"
+#include "td/actor/ConcurrentScheduler.h"
+
+#include "td/utils/algorithm.h"
+#include "td/utils/as.h"
 #include "td/utils/base64.h"
 #include "td/utils/buffer.h"
+#include "td/utils/common.h"
 #include "td/utils/crypto.h"
 #include "td/utils/format.h"
 #include "td/utils/Gzip.h"
 #include "td/utils/logging.h"
 #include "td/utils/misc.h"
+#include "td/utils/Promise.h"
 #include "td/utils/Random.h"
 #include "td/utils/Slice.h"
+#include "td/utils/SliceBuilder.h"
 #include "td/utils/Status.h"
 #include "td/utils/tests.h"
 #include "td/utils/tl_helpers.h"
@@ -43,17 +55,14 @@
 #include 
 #include 
 
-REGISTER_TESTS(secret);
-
 namespace my_api {
 
 using namespace td;
 
-//messages_getDhConfig
 class messages_getDhConfig {
  public:
-  int32 version_;
-  int32 random_length_;
+  int32 version_{};
+  int32 random_length_{};
 
   messages_getDhConfig() = default;
 
@@ -70,7 +79,6 @@ class messages_getDhConfig {
   }
 };
 
-//InputUser
 class InputUser {
  public:
   static tl_object_ptr fetch(TlBufferParser &p);
@@ -78,8 +86,8 @@ class InputUser {
 
 class inputUser final : public InputUser {
  public:
-  int32 user_id_;
-  int64 access_hash_;
+  int64 user_id_{};
+  int64 access_hash_{};
 
   static const int32 ID = -668391402;
   inputUser() = default;
@@ -92,6 +100,7 @@ class inputUser final : public InputUser {
   {
   }
 };
+
 tl_object_ptr InputUser::fetch(TlBufferParser &p) {
 #define FAIL(error)   \
   p.set_error(error); \
@@ -109,36 +118,28 @@ tl_object_ptr InputUser::fetch(TlBufferParser &p) {
 class messages_requestEncryption final {
  public:
   tl_object_ptr user_id_;
-  int32 random_id_;
+  int32 random_id_{};
   BufferSlice g_a_;
 
   static const int32 ID = -162681021;
   messages_requestEncryption();
 
   explicit messages_requestEncryption(TlBufferParser &p)
-#define FAIL(error) p.set_error(error)
       : user_id_(TlFetchObject::parse(p))
       , random_id_(TlFetchInt::parse(p))
-      , g_a_(TlFetchBytes::parse(p))
-#undef FAIL
-  {
+      , g_a_(TlFetchBytes::parse(p)) {
   }
 };
 
 class inputEncryptedChat final {
  public:
-  int32 chat_id_;
-  int64 access_hash_;
+  int32 chat_id_{};
+  int64 access_hash_{};
 
   inputEncryptedChat() = default;
 
   static const int32 ID = -247351839;
-  explicit inputEncryptedChat(TlBufferParser &p)
-#define FAIL(error) p.set_error(error)
-      : chat_id_(TlFetchInt::parse(p))
-      , access_hash_(TlFetchLong::parse(p))
-#undef FAIL
-  {
+  explicit inputEncryptedChat(TlBufferParser &p) : chat_id_(TlFetchInt::parse(p)), access_hash_(TlFetchLong::parse(p)) {
   }
   static tl_object_ptr fetch(TlBufferParser &p) {
     return make_tl_object(p);
@@ -149,56 +150,49 @@ class messages_acceptEncryption final {
  public:
   tl_object_ptr peer_;
   BufferSlice g_b_;
-  int64 key_fingerprint_;
+  int64 key_fingerprint_{};
 
   messages_acceptEncryption() = default;
 
   static const int32 ID = 1035731989;
 
   explicit messages_acceptEncryption(TlBufferParser &p)
-#define FAIL(error) p.set_error(error)
       : peer_(TlFetchBoxed, -247351839>::parse(p))
       , g_b_(TlFetchBytes::parse(p))
-      , key_fingerprint_(TlFetchLong::parse(p))
-#undef FAIL
-  {
+      , key_fingerprint_(TlFetchLong::parse(p)) {
   }
 };
 
 class messages_sendEncryptedService final {
  public:
   tl_object_ptr peer_;
-  int64 random_id_;
+  int64 random_id_{};
   BufferSlice data_;
 
   messages_sendEncryptedService() = default;
   static const int32 ID = 852769188;
   explicit messages_sendEncryptedService(TlBufferParser &p)
-#define FAIL(error) p.set_error(error)
       : peer_(TlFetchBoxed, -247351839>::parse(p))
       , random_id_(TlFetchLong::parse(p))
-      , data_(TlFetchBytes::parse(p))
-#undef FAIL
-  {
+      , data_(TlFetchBytes::parse(p)) {
   }
 };
 
 class messages_sendEncrypted final {
  public:
+  int32 flags_;
   tl_object_ptr peer_;
-  int64 random_id_;
+  int64 random_id_{};
   BufferSlice data_;
 
   messages_sendEncrypted() = default;
-  static const int32 ID = -1451792525;
+  static const int32 ID = 1157265941;
 
   explicit messages_sendEncrypted(TlBufferParser &p)
-#define FAIL(error) p.set_error(error)
-      : peer_(TlFetchBoxed, -247351839>::parse(p))
+      : flags_(TlFetchInt::parse(p))
+      , peer_(TlFetchBoxed, -247351839>::parse(p))
       , random_id_(TlFetchLong::parse(p))
-      , data_(TlFetchBytes::parse(p))
-#undef FAIL
-  {
+      , data_(TlFetchBytes::parse(p)) {
   }
 };
 
@@ -217,16 +211,16 @@ static void downcast_call(TlBufferParser &p, F &&f) {
     case messages_sendEncryptedService::ID:
       return f(*make_tl_object(p));
     default:
-      CHECK(0) << id;
+      LOG(ERROR) << "Unknown constructor " << id;
       UNREACHABLE();
   }
 }
 
 class messages_dhConfig final {
  public:
-  int32 g_;
+  int32 g_{};
   BufferSlice p_;
-  int32 version_;
+  int32 version_{};
   BufferSlice random_;
 
   messages_dhConfig() = default;
@@ -258,17 +252,17 @@ class messages_dhConfig final {
 
 class encryptedChat final {
  public:
-  int32 id_;
-  int64 access_hash_;
-  int32 date_;
-  int32 admin_id_;
-  int32 participant_id_;
+  int32 id_{};
+  int64 access_hash_{};
+  int32 date_{};
+  int64 admin_id_{};
+  int64 participant_id_{};
   BufferSlice g_a_or_b_;
-  int64 key_fingerprint_;
+  int64 key_fingerprint_{};
 
   encryptedChat() = default;
 
-  encryptedChat(int32 id_, int64 access_hash_, int32 date_, int32 admin_id_, int32 participant_id_,
+  encryptedChat(int32 id_, int64 access_hash_, int32 date_, int64 admin_id_, int64 participant_id_,
                 BufferSlice &&g_a_or_b_, int64 key_fingerprint_)
       : id_(id_)
       , access_hash_(access_hash_)
@@ -309,7 +303,7 @@ class encryptedChat final {
 
 class messages_sentEncryptedMessage final {
  public:
-  int32 date_;
+  int32 date_{};
 
   messages_sentEncryptedMessage() = default;
 
@@ -342,32 +336,32 @@ static string prime_base64 =
     "WC2xF40WnGvEZbDW_5yjko_vW5rk5Bj8Feg-vqD4f6n_Xu1wBQ3tKEn0e_lZ2VaFDOkphR8NgRX2NbEF7i5OFdBLJFS_b0-t8DSxBAMRnNjjuS_MW"
     "w";
 
-class FakeDhCallback : public DhCallback {
+class FakeDhCallback final : public mtproto::DhCallback {
  public:
-  int is_good_prime(Slice prime_str) const override {
+  int is_good_prime(Slice prime_str) const final {
     auto it = cache.find(prime_str.str());
     if (it == cache.end()) {
       return -1;
     }
     return it->second;
   }
-  void add_good_prime(Slice prime_str) const override {
+  void add_good_prime(Slice prime_str) const final {
     cache[prime_str.str()] = 1;
   }
-  void add_bad_prime(Slice prime_str) const override {
+  void add_bad_prime(Slice prime_str) const final {
     cache[prime_str.str()] = 0;
   }
   mutable std::map cache;
 };
 
-class FakeBinlog
+class FakeBinlog final
     : public BinlogInterface
     , public Actor {
  public:
   FakeBinlog() {
     register_actor("FakeBinlog", this).release();
   }
-  void force_sync(Promise<> promise) override {
+  void force_sync(Promise<> promise) final {
     if (pending_events_.empty()) {
       pending_events_.emplace_back();
     }
@@ -385,15 +379,15 @@ class FakeBinlog
       }
     }
   }
-  void force_flush() override {
+  void force_flush() final {
   }
 
-  uint64 next_id() override {
+  uint64 next_id() final {
     auto res = last_id_;
     last_id_++;
     return res;
   }
-  uint64 next_id(int32 shift) override {
+  uint64 next_id(int32 shift) final {
     auto res = last_id_;
     last_id_ += shift;
     return res;
@@ -418,16 +412,16 @@ class FakeBinlog
     pending_events_.clear();
   }
 
-  void change_key(DbKey key, Promise<> promise) override {
+  void change_key(DbKey key, Promise<> promise) final {
   }
 
  protected:
-  void close_impl(Promise<> promise) override {
+  void close_impl(Promise<> promise) final {
   }
-  void close_and_destroy_impl(Promise<> promise) override {
+  void close_and_destroy_impl(Promise<> promise) final {
   }
-  void add_raw_event_impl(uint64 id, BufferSlice &&raw_event, Promise<> promise) override {
-    auto event = BinlogEvent(std::move(raw_event));
+  void add_raw_event_impl(uint64 id, BufferSlice &&raw_event, Promise<> promise, BinlogDebugInfo info) final {
+    auto event = BinlogEvent(std::move(raw_event), info);
     LOG(INFO) << "ADD EVENT: " << event.id_ << " " << event;
     pending_events_.emplace_back();
     pending_events_.back().event = std::move(event);
@@ -441,20 +435,18 @@ class FakeBinlog
     has_request_sync = false;
     auto pos = static_cast(Random::fast_uint64() % pending_events_.size());
     // pos = pending_events_.size() - 1;
-    std::vector> promises;
+    td::vector> promises;
     for (size_t i = 0; i <= pos; i++) {
       auto &pending = pending_events_[i];
       auto event = std::move(pending.event);
       if (!event.empty()) {
         LOG(INFO) << "SAVE EVENT: " << event.id_ << " " << event;
-        events_processor_.add_event(std::move(event));
+        events_processor_.add_event(std::move(event)).ensure();
       }
       append(promises, std::move(pending.promises_));
     }
     pending_events_.erase(pending_events_.begin(), pending_events_.begin() + pos + 1);
-    for (auto &promise : promises) {
-      promise.set_value(Unit());
-    }
+    set_promises(promises);
 
     for (auto &event : pending_events_) {
       if (event.sync_flag) {
@@ -463,10 +455,10 @@ class FakeBinlog
       }
     }
   }
-  void timeout_expired() override {
+  void timeout_expired() final {
     do_force_sync();
   }
-  void wakeup() override {
+  void wakeup() final {
     if (has_request_sync) {
       do_force_sync();
     }
@@ -478,42 +470,16 @@ class FakeBinlog
   struct PendingEvent {
     BinlogEvent event;
     bool sync_flag = false;
-    std::vector> promises_;
+    td::vector> promises_;
   };
 
   std::vector pending_events_;
 };
 
 using FakeKeyValue = BinlogKeyValue;
-class OldFakeKeyValue : public KeyValueSyncInterface {
-  SeqNo set(string key, string value) override {
-    kv_[key] = value;
-    return 0;
-  }
-
-  SeqNo erase(const string &key) override {
-    kv_.erase(key);
-    return 0;
-  }
-
-  bool isset(const string &key) override {
-    return kv_.count(key) > 0;
-  }
-
-  string get(const string &key) override {
-    auto it = kv_.find(key);
-    if (it != kv_.end()) {
-      return it->second;
-    }
-    return string();
-  }
-
- private:
-  std::map kv_;
-};
 
 class Master;
-class FakeSecretChatContext : public SecretChatActor::Context {
+class FakeSecretChatContext final : public SecretChatActor::Context {
  public:
   FakeSecretChatContext(std::shared_ptr binlog, std::shared_ptr key_value,
                         std::shared_ptr close_flag, ActorShared master)
@@ -521,28 +487,28 @@ class FakeSecretChatContext : public SecretChatActor::Context {
       , key_value_(std::move(key_value))
       , close_flag_(std::move(close_flag))
       , master_(std::move(master)) {
-    secret_chat_db_ = std::make_unique(key_value_, 1);
+    secret_chat_db_ = std::make_shared(key_value_, 1);
     net_query_creator_.stop_check();  // :(
   }
-  DhCallback *dh_callback() override {
+  mtproto::DhCallback *dh_callback() final {
     return &fake_dh_callback_;
   }
-  NetQueryCreator &net_query_creator() override {
+  NetQueryCreator &net_query_creator() final {
     return net_query_creator_;
   }
-  int32 unix_time() override {
+  int32 unix_time() final {
     return static_cast(std::time(nullptr));
   }
-  bool close_flag() override {
+  bool close_flag() final {
     return *close_flag_;
   }
-  BinlogInterface *binlog() override {
+  BinlogInterface *binlog() final {
     return binlog_.get();
   }
-  SecretChatDb *secret_chat_db() override {
+  SecretChatDb *secret_chat_db() final {
     return secret_chat_db_.get();
   }
-  std::shared_ptr dh_config() override {
+  std::shared_ptr dh_config() final {
     static auto config = [] {
       DhConfig dh_config;
       dh_config.version = 12;
@@ -553,39 +519,37 @@ class FakeSecretChatContext : public SecretChatActor::Context {
 
     return config;
   }
-  void set_dh_config(std::shared_ptr dh_config) override {
+  void set_dh_config(std::shared_ptr dh_config) final {
     // empty
   }
 
-  bool get_config_option_boolean(const string &name) const override {
+  bool get_config_option_boolean(const string &name) const final {
     return false;
   }
 
   // We don't want to expose the whole NetQueryDispatcher, MessagesManager and ContactsManager.
   // So it is more clear which parts of MessagesManager is really used. And it is much easier to create tests.
-  void send_net_query(NetQueryPtr query, ActorShared callback, bool ordered) override;
+  void send_net_query(NetQueryPtr query, ActorShared callback, bool ordered) final;
 
   void on_update_secret_chat(int64 access_hash, UserId user_id, SecretChatState state, bool is_outbound, int32 ttl,
-                             int32 date, string key_hash, int32 layer) override {
+                             int32 date, string key_hash, int32 layer, FolderId initial_folder_id) final {
   }
 
-  void on_inbound_message(UserId user_id, MessageId message_id, int32 date,
-                          tl_object_ptr file,
-                          tl_object_ptr message, Promise<>) override;
+  void on_inbound_message(UserId user_id, MessageId message_id, int32 date, unique_ptr file,
+                          tl_object_ptr message, Promise<>) final;
 
-  void on_send_message_error(int64 random_id, Status error, Promise<>) override;
-  void on_send_message_ack(int64 random_id) override;
-  void on_send_message_ok(int64 random_id, MessageId message_id, int32 date,
-                          tl_object_ptr file, Promise<>) override;
-  void on_delete_messages(std::vector random_id, Promise<>) override;
-  void on_flush_history(MessageId, Promise<>) override;
-  void on_read_message(int64, Promise<>) override;
+  void on_send_message_error(int64 random_id, Status error, Promise<>) final;
+  void on_send_message_ack(int64 random_id) final;
+  void on_send_message_ok(int64 random_id, MessageId message_id, int32 date, unique_ptr file,
+                          Promise<>) final;
+  void on_delete_messages(std::vector random_id, Promise<>) final;
+  void on_flush_history(bool, MessageId, Promise<>) final;
+  void on_read_message(int64, Promise<>) final;
 
-  void on_screenshot_taken(UserId user_id, MessageId message_id, int32 date, int64 random_id,
-                           Promise<> promise) override {
+  void on_screenshot_taken(UserId user_id, MessageId message_id, int32 date, int64 random_id, Promise<> promise) final {
   }
   void on_set_ttl(UserId user_id, MessageId message_id, int32 date, int32 ttl, int64 random_id,
-                  Promise<> promise) override {
+                  Promise<> promise) final {
   }
 
  private:
@@ -598,13 +562,13 @@ class FakeSecretChatContext : public SecretChatActor::Context {
 
   std::shared_ptr secret_chat_db_;
 };
-NetQueryCreator FakeSecretChatContext::net_query_creator_;
+NetQueryCreator FakeSecretChatContext::net_query_creator_{nullptr};
 
-class Master : public Actor {
+class Master final : public Actor {
  public:
   explicit Master(Status *status) : status_(status) {
   }
-  class SecretChatProxy : public Actor {
+  class SecretChatProxy final : public Actor {
    public:
     SecretChatProxy(string name, ActorShared parent) : name_(std::move(name)) {
       binlog_ = std::make_shared();
@@ -615,8 +579,8 @@ class Master : public Actor {
       parent_ = parent.get();
       parent_token_ = parent.token();
       actor_ = create_actor(
-          "SecretChat " + name_, 123,
-          std::make_unique(binlog_, key_value_, close_flag_, std::move(parent)), true);
+          PSLICE() << "SecretChat " << name_, 123,
+          td::make_unique(binlog_, key_value_, close_flag_, std::move(parent)), true);
       on_binlog_replay_finish();
     }
 
@@ -624,12 +588,11 @@ class Master : public Actor {
 
     void add_inbound_message(int32 chat_id, BufferSlice data, uint64 crc) {
       CHECK(crc64(data.as_slice()) == crc);
-      auto event = std::make_unique();
-      event->qts = 0;
+      auto event = make_unique();
       event->chat_id = chat_id;
       event->date = 0;
       event->encrypted_message = std::move(data);
-      event->qts_ack = PromiseCreator::lambda(
+      event->promise = PromiseCreator::lambda(
           [actor_id = actor_id(this), chat_id, data = event->encrypted_message.copy(), crc](Result<> result) mutable {
             if (result.is_ok()) {
               LOG(INFO) << "FINISH add_inbound_message " << tag("crc", crc);
@@ -642,7 +605,7 @@ class Master : public Actor {
       add_event(Event::delayed_closure(&SecretChatActor::add_inbound_message, std::move(event)));
     }
 
-    void send_message(tl_object_ptr message) {
+    void send_message(tl_object_ptr message) {
       BufferSlice serialized_message(serialize(*message));
       auto resend_promise = PromiseCreator::lambda(
           [actor_id = actor_id(this), serialized_message = std::move(serialized_message)](Result<> result) mutable {
@@ -671,7 +634,7 @@ class Master : public Actor {
     int32 binlog_generation_ = 0;
     void sync_binlog(int32 binlog_generation, Promise<> promise) {
       if (binlog_generation != binlog_generation_) {
-        return promise.set_error(Status::Error("binlog generation mismatch"));
+        return promise.set_error(Status::Error("Binlog generation mismatch"));
       }
       binlog_->force_sync(std::move(promise));
     }
@@ -697,28 +660,28 @@ class Master : public Actor {
       key_value_->external_init_finish(binlog_);
 
       actor_ = create_actor(
-          "SecretChat " + name_, 123,
-          std::make_unique(binlog_, key_value_, close_flag_,
-                                                  ActorShared(parent_, parent_token_)),
+          PSLICE() << "SecretChat " << name_, 123,
+          td::make_unique(binlog_, key_value_, close_flag_,
+                                                 ActorShared(parent_, parent_token_)),
           true);
 
       for (auto &event : events) {
         CHECK(event.type_ == LogEvent::HandlerType::SecretChats);
-        auto r_message = logevent::SecretChatEvent::from_buffer_slice(event.data_as_buffer_slice());
+        auto r_message = log_event::SecretChatEvent::from_buffer_slice(event.data_as_buffer_slice());
         LOG_IF(FATAL, r_message.is_error()) << "Failed to deserialize event: " << r_message.error();
         auto message = r_message.move_as_ok();
-        message->set_logevent_id(event.id_);
+        message->set_log_event_id(event.id_);
         LOG(INFO) << "Process binlog event " << *message;
         switch (message->get_type()) {
-          case logevent::SecretChatEvent::Type::InboundSecretMessage:
+          case log_event::SecretChatEvent::Type::InboundSecretMessage:
             send_closure_later(actor_, &SecretChatActor::replay_inbound_message,
-                               std::unique_ptr(
-                                   static_cast(message.release())));
+                               unique_ptr(
+                                   static_cast(message.release())));
             break;
-          case logevent::SecretChatEvent::Type::OutboundSecretMessage:
+          case log_event::SecretChatEvent::Type::OutboundSecretMessage:
             send_closure_later(actor_, &SecretChatActor::replay_outbound_message,
-                               std::unique_ptr(
-                                   static_cast(message.release())));
+                               unique_ptr(
+                                   static_cast(message.release())));
             break;
           default:
             UNREACHABLE();
@@ -729,7 +692,7 @@ class Master : public Actor {
     }
     void on_binlog_replay_finish() {
       ready_ = true;
-      LOG(INFO) << "on_binlog_replay_finish!";
+      LOG(INFO) << "Finish replay binlog";
       send_closure(actor_, &SecretChatActor::binlog_replay_finish);
       for (auto &event : pending_events_) {
         send_event(actor_, std::move(event));
@@ -769,7 +732,7 @@ class Master : public Actor {
     }
 
     int32 bad_cnt_ = 0;
-    void timeout_expired() override {
+    void timeout_expired() final {
       LOG(INFO) << "TIMEOUT EXPIRED";
       if (events_cnt_ < 4) {
         bad_cnt_++;
@@ -795,21 +758,23 @@ class Master : public Actor {
   auto &to() {
     return get_by_id(3 - get_link_token());
   }
-  void start_up() override {
-    set_context(std::make_shared());
+  void start_up() final {
+    auto old_context = set_context(std::make_shared());
     alice_ = create_actor("SecretChatProxy alice", "alice", actor_shared(this, 1));
     bob_ = create_actor("SecretChatProxy bob", "bob", actor_shared(this, 2));
-    send_closure(alice_->get_actor_unsafe()->actor_, &SecretChatActor::create_chat, 2, 0, 123,
-                 PromiseCreator::lambda([actor_id = actor_id(this)](Result res) {
-                   send_closure(actor_id, &Master::got_secret_chat_id, std::move(res), 0);
+    send_closure(alice_.get_actor_unsafe()->actor_, &SecretChatActor::create_chat, UserId(static_cast(2)), 0,
+                 123, PromiseCreator::lambda([actor_id = actor_id(this)](Result res) {
+                   send_closure(actor_id, &Master::got_secret_chat_id, std::move(res), false);
                  }));
   }
-  void got_secret_chat_id(Result res, int) {  // second parameter is needed to workaround clang bug
+
+  void got_secret_chat_id(Result res, bool dummy) {
     CHECK(res.is_ok());
     auto id = res.move_as_ok();
     LOG(INFO) << "SecretChatId = " << id;
   }
-  bool can_fail(NetQueryPtr &query) {
+
+  static bool can_fail(NetQueryPtr &query) {
     static int cnt = 20;
     if (cnt > 0) {
       cnt--;
@@ -821,17 +786,18 @@ class Master : public Actor {
     }
     return false;
   }
+
   void send_net_query(NetQueryPtr query, ActorShared callback, bool ordered) {
-    if (can_fail(query) && Random::fast(0, 1) == 0) {
+    if (can_fail(query) && Random::fast_bool()) {
       LOG(INFO) << "Fail query " << query;
       auto resend_promise =
-          PromiseCreator::lambda([id = actor_shared(this, get_link_token()), callback_actor = callback.get(),
+          PromiseCreator::lambda([self = actor_shared(this, get_link_token()), callback_actor = callback.get(),
                                   callback_token = callback.token()](Result r_net_query) mutable {
             if (r_net_query.is_error()) {
-              id.release();
+              self.release();
               return;
             }
-            send_closure(std::move(id), &Master::send_net_query, r_net_query.move_as_ok(),
+            send_closure(std::move(self), &Master::send_net_query, r_net_query.move_as_ok(),
                          ActorShared(callback_actor, callback_token), true);
           });
       query->set_error(Status::Error(429, "Test error"));
@@ -866,31 +832,33 @@ class Master : public Actor {
     config.version_ = 12;
     auto storer = TLObjectStorer(config);
     BufferSlice answer(storer.size());
-    storer.store(answer.as_slice().ubegin());
+    auto real_size = storer.store(answer.as_slice().ubegin());
+    CHECK(real_size == answer.size());
     net_query->set_ok(std::move(answer));
     send_closure(std::move(callback), &NetQueryCallback::on_result, std::move(net_query));
   }
   void process_net_query(my_api::messages_requestEncryption &&request_encryption, NetQueryPtr net_query,
                          ActorShared callback) {
     CHECK(get_link_token() == 1);
-    send_closure(alice_->get_actor_unsafe()->actor_, &SecretChatActor::update_chat,
+    send_closure(alice_.get_actor_unsafe()->actor_, &SecretChatActor::update_chat,
                  make_tl_object(123, 321, 0, 1, 2));
-    send_closure(
-        bob_->get_actor_unsafe()->actor_, &SecretChatActor::update_chat,
-        make_tl_object(123, 321, 0, 1, 2, request_encryption.g_a_.clone()));
+    send_closure(bob_.get_actor_unsafe()->actor_, &SecretChatActor::update_chat,
+                 make_tl_object(0, false, 123, 321, 0, 1, 2,
+                                                                      request_encryption.g_a_.clone()));
     net_query->clear();
   }
   void process_net_query(my_api::messages_acceptEncryption &&request_encryption, NetQueryPtr net_query,
                          ActorShared callback) {
     CHECK(get_link_token() == 2);
-    send_closure(alice_->get_actor_unsafe()->actor_, &SecretChatActor::update_chat,
+    send_closure(alice_.get_actor_unsafe()->actor_, &SecretChatActor::update_chat,
                  make_tl_object(123, 321, 0, 1, 2, request_encryption.g_b_.clone(),
                                                              request_encryption.key_fingerprint_));
 
     my_api::encryptedChat encrypted_chat(123, 321, 0, 1, 2, BufferSlice(), request_encryption.key_fingerprint_);
     auto storer = TLObjectStorer(encrypted_chat);
     BufferSlice answer(storer.size());
-    storer.store(answer.as_slice().ubegin());
+    auto real_size = storer.store(answer.as_slice().ubegin());
+    CHECK(real_size == answer.size());
     net_query->set_ok(std::move(answer));
     send_closure(std::move(callback), &NetQueryCallback::on_result, std::move(net_query));
     send_closure(alice_, &SecretChatProxy::start_test);
@@ -898,49 +866,47 @@ class Master : public Actor {
     send_ping(1, 5000);
     set_timeout_in(1);
   }
-  void timeout_expired() override {
+  void timeout_expired() final {
     send_message(1, "oppa");
     send_message(2, "appo");
     set_timeout_in(1);
   }
-  void send_ping(int id, int cnt) {
+  void send_ping(int32 id, int cnt) {
     if (cnt % 200 == 0) {
-      LOG(ERROR) << "send ping " << tag("id", id) << tag("cnt", cnt);
+      LOG(ERROR) << "Send ping " << tag("id", id) << tag("cnt", cnt);
     } else {
-      LOG(INFO) << "send ping " << tag("id", id) << tag("cnt", cnt);
+      LOG(INFO) << "Send ping " << tag("id", id) << tag("cnt", cnt);
     }
     string text = PSTRING() << "PING: " << cnt;
     send_message(id, std::move(text));
   }
-  void send_message(int id, string text) {
+  void send_message(int32 id, string text) {
     auto random_id = Random::secure_int64();
-    LOG(INFO) << "send message: " << tag("id", id) << tag("text", text) << tag("random_id", random_id);
+    LOG(INFO) << "Send message: " << tag("id", id) << tag("text", text) << tag("random_id", random_id);
     sent_messages_[random_id] = Message{id, text};
     send_closure(get_by_id(id), &SecretChatProxy::send_message,
-                 secret_api::make_object(0, random_id, 0, text, Auto(), Auto(), Auto(),
-                                                                       Auto(), 0));
+                 secret_api::make_object(0, false /*ignored*/, random_id, 0, text, Auto(),
+                                                                       Auto(), Auto(), Auto(), 0));
   }
   void process_net_query(my_api::messages_sendEncryptedService &&message, NetQueryPtr net_query,
                          ActorShared callback) {
-    process_net_query_send_enrypted(std::move(message.data_), std::move(net_query), std::move(callback));
+    process_net_query_send_encrypted(std::move(message.data_), std::move(net_query), std::move(callback));
   }
   void process_net_query(my_api::messages_sendEncrypted &&message, NetQueryPtr net_query,
                          ActorShared callback) {
-    process_net_query_send_enrypted(std::move(message.data_), std::move(net_query), std::move(callback));
+    process_net_query_send_encrypted(std::move(message.data_), std::move(net_query), std::move(callback));
   }
-  void process_net_query_send_enrypted(BufferSlice data, NetQueryPtr net_query,
-                                       ActorShared callback) {
-    my_api::messages_sentEncryptedMessage sent_message;
-    sent_message.date_ = 0;
-    auto storer = TLObjectStorer(sent_message);
-    BufferSlice answer(storer.size());
-    storer.store(answer.as_slice().ubegin());
+  void process_net_query_send_encrypted(BufferSlice data, NetQueryPtr net_query,
+                                        ActorShared callback) {
+    BufferSlice answer(8);
+    answer.as_slice().fill(0);
+    as(answer.as_slice().begin()) = static_cast(my_api::messages_sentEncryptedMessage::ID);
     net_query->set_ok(std::move(answer));
     send_closure(std::move(callback), &NetQueryCallback::on_result, std::move(net_query));
 
     // We can't loose updates yet :(
     auto crc = crc64(data.as_slice());
-    LOG(INFO) << "send SecretChatProxy::add_inbound_message" << tag("crc", crc);
+    LOG(INFO) << "Send SecretChatProxy::add_inbound_message" << tag("crc", crc);
     send_closure(to(), &SecretChatProxy::add_inbound_message, narrow_cast(3 - get_link_token()), std::move(data),
                  crc);
   }
@@ -967,10 +933,10 @@ class Master : public Actor {
   }
   void on_send_message_error(int64 random_id, Status error, Promise<> promise) {
     promise.set_value(Unit());
-    LOG(INFO) << "on_send_message_error: " << tag("random_id", random_id) << error;
+    LOG(INFO) << "Receive send message error: " << tag("random_id", random_id) << error;
     auto it = sent_messages_.find(random_id);
     if (it == sent_messages_.end()) {
-      LOG(INFO) << "TODO: try fix errors about message after it is sent";
+      LOG(INFO) << "TODO: try to fix errors about message after it is sent";
       return;
     }
     CHECK(it != sent_messages_.end());
@@ -980,10 +946,10 @@ class Master : public Actor {
   }
   void on_send_message_ok(int64 random_id, Promise<> promise) {
     promise.set_value(Unit());
-    LOG(INFO) << "on_send_message_ok: " << tag("random_id", random_id);
+    LOG(INFO) << "Receive send message ok: " << tag("random_id", random_id);
     auto it = sent_messages_.find(random_id);
     if (it == sent_messages_.end()) {
-      LOG(INFO) << "TODO: try fix errors about message after it is sent";
+      LOG(INFO) << "TODO: try to fix errors about message after it is sent";
       return;
     }
     CHECK(it != sent_messages_.end());
@@ -1000,7 +966,7 @@ class Master : public Actor {
   };
   std::map sent_messages_;
 
-  void hangup_shared() override {
+  void hangup_shared() final {
     LOG(INFO) << "GOT HANGUP: " << get_link_token();
     send_closure(from(), &SecretChatProxy::on_closed);
   }
@@ -1010,7 +976,7 @@ void FakeSecretChatContext::send_net_query(NetQueryPtr query, ActorShared file,
+                                               unique_ptr file,
                                                tl_object_ptr message, Promise<> promise) {
   send_closure(master_, &Master::on_inbound_message, message->message_, std::move(promise));
 }
@@ -1020,24 +986,22 @@ void FakeSecretChatContext::on_send_message_error(int64 random_id, Status error,
 void FakeSecretChatContext::on_send_message_ack(int64 random_id) {
 }
 void FakeSecretChatContext::on_send_message_ok(int64 random_id, MessageId message_id, int32 date,
-                                               tl_object_ptr file, Promise<> promise) {
+                                               unique_ptr file, Promise<> promise) {
   send_closure(master_, &Master::on_send_message_ok, random_id, std::move(promise));
 }
 void FakeSecretChatContext::on_delete_messages(std::vector random_id, Promise<> promise) {
   promise.set_value(Unit());
 }
-void FakeSecretChatContext::on_flush_history(MessageId, Promise<> promise) {
-  promise.set_error(Status::Error("unsupported"));
+void FakeSecretChatContext::on_flush_history(bool, MessageId, Promise<> promise) {
+  promise.set_error(Status::Error("Unsupported"));
 }
 void FakeSecretChatContext::on_read_message(int64, Promise<> promise) {
-  promise.set_error(Status::Error("unsupported"));
+  promise.set_error(Status::Error("Unsupported"));
 }
 
 TEST(Secret, go) {
-  SET_VERBOSITY_LEVEL(VERBOSITY_NAME(ERROR));
-  ConcurrentScheduler sched;
-  int threads_n = 0;
-  sched.init(threads_n);
+  return;
+  ConcurrentScheduler sched(0, 0);
 
   Status result;
   sched.create_actor_unsafe(0, "HandshakeTestActor", &result).release();
diff --git a/protocols/Telegram/tdlib/td/test/secure_storage.cpp b/protocols/Telegram/tdlib/td/test/secure_storage.cpp
new file mode 100644
index 0000000000..774dead5ac
--- /dev/null
+++ b/protocols/Telegram/tdlib/td/test/secure_storage.cpp
@@ -0,0 +1,70 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/telegram/SecureStorage.h"
+
+#include "td/utils/buffer.h"
+#include "td/utils/filesystem.h"
+#include "td/utils/port/path.h"
+#include "td/utils/SliceBuilder.h"
+#include "td/utils/tests.h"
+
+TEST(SecureStorage, secret) {
+  auto secret = td::secure_storage::Secret::create_new();
+  td::string key = "cucumber";
+  auto encrypted_secret = secret.encrypt(key, "", td::secure_storage::EnryptionAlgorithm::Sha512);
+  ASSERT_TRUE(encrypted_secret.as_slice() != secret.as_slice());
+  auto decrypted_secret = encrypted_secret.decrypt(key, "", td::secure_storage::EnryptionAlgorithm::Sha512).ok();
+  ASSERT_TRUE(secret.as_slice() == decrypted_secret.as_slice());
+  ASSERT_TRUE(encrypted_secret.decrypt("notcucumber", "", td::secure_storage::EnryptionAlgorithm::Sha512).is_error());
+}
+
+TEST(SecureStorage, simple) {
+  td::BufferSlice value("Small tale about cucumbers");
+  auto value_secret = td::secure_storage::Secret::create_new();
+
+  {
+    td::secure_storage::BufferSliceDataView value_view(value.copy());
+    td::BufferSlice prefix = td::secure_storage::gen_random_prefix(value_view.size());
+    td::secure_storage::BufferSliceDataView prefix_view(std::move(prefix));
+    td::secure_storage::ConcatDataView full_value_view(prefix_view, value_view);
+    auto hash = td::secure_storage::calc_value_hash(full_value_view).move_as_ok();
+
+    td::secure_storage::Encryptor encryptor(
+        td::secure_storage::calc_aes_cbc_state_sha512(PSLICE() << value_secret.as_slice() << hash.as_slice()),
+        full_value_view);
+    auto encrypted_value = encryptor.pread(0, encryptor.size()).move_as_ok();
+
+    td::secure_storage::Decryptor decryptor(
+        td::secure_storage::calc_aes_cbc_state_sha512(PSLICE() << value_secret.as_slice() << hash.as_slice()));
+    auto res = decryptor.append(encrypted_value.copy()).move_as_ok();
+    auto decrypted_hash = decryptor.finish().ok();
+    ASSERT_TRUE(decrypted_hash.as_slice() == hash.as_slice());
+    ASSERT_TRUE(res.as_slice() == value.as_slice());
+  }
+
+  {
+    auto encrypted_value = td::secure_storage::encrypt_value(value_secret, value.as_slice()).move_as_ok();
+    auto decrypted_value =
+        td::secure_storage::decrypt_value(value_secret, encrypted_value.hash, encrypted_value.data.as_slice())
+            .move_as_ok();
+    ASSERT_TRUE(decrypted_value.as_slice() == value.as_slice());
+  }
+
+  {
+    td::string value_path = "value.txt";
+    td::string encrypted_path = "encrypted.txt";
+    td::string decrypted_path = "decrypted.txt";
+    td::unlink(value_path).ignore();
+    td::unlink(encrypted_path).ignore();
+    td::unlink(decrypted_path).ignore();
+    td::string file_value(100000, 'a');
+    td::write_file(value_path, file_value).ensure();
+    auto hash = td::secure_storage::encrypt_file(value_secret, value_path, encrypted_path).move_as_ok();
+    td::secure_storage::decrypt_file(value_secret, hash, encrypted_path, decrypted_path).ensure();
+    ASSERT_TRUE(td::read_file(decrypted_path).move_as_ok().as_slice() == file_value);
+  }
+}
diff --git a/protocols/Telegram/tdlib/td/test/set_with_position.cpp b/protocols/Telegram/tdlib/td/test/set_with_position.cpp
new file mode 100644
index 0000000000..c65b8bc362
--- /dev/null
+++ b/protocols/Telegram/tdlib/td/test/set_with_position.cpp
@@ -0,0 +1,263 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/telegram/SetWithPosition.h"
+
+#include "td/utils/algorithm.h"
+#include "td/utils/common.h"
+#include "td/utils/misc.h"
+#include "td/utils/Random.h"
+#include "td/utils/tests.h"
+
+#include 
+#include 
+#include 
+#include 
+
+template 
+class OldSetWithPosition {
+ public:
+  void add(T value) {
+    if (td::contains(values_, value)) {
+      return;
+    }
+    values_.push_back(value);
+  }
+  void remove(T value) {
+    auto it = std::find(values_.begin(), values_.end(), value);
+    if (it == values_.end()) {
+      return;
+    }
+    std::size_t i = it - values_.begin();
+    values_.erase(it);
+    if (pos_ > i) {
+      pos_--;
+    }
+  }
+  void reset_position() {
+    pos_ = 0;
+  }
+  T next() {
+    CHECK(has_next());
+    return values_[pos_++];
+  }
+  bool has_next() const {
+    return pos_ < values_.size();
+  }
+  void merge(OldSetWithPosition &&other) {
+    OldSetWithPosition res;
+    for (std::size_t i = 0; i < pos_; i++) {
+      res.add(values_[i]);
+    }
+    for (std::size_t i = 0; i < other.pos_; i++) {
+      res.add(other.values_[i]);
+    }
+    res.pos_ = res.values_.size();
+    for (std::size_t i = pos_; i < values_.size(); i++) {
+      res.add(values_[i]);
+    }
+    for (std::size_t i = other.pos_; i < other.values_.size(); i++) {
+      res.add(other.values_[i]);
+    }
+    *this = std::move(res);
+  }
+
+ private:
+  td::vector values_;
+  std::size_t pos_{0};
+};
+
+template  class SetWithPosition>
+class CheckedSetWithPosition {
+ public:
+  void add(int x) {
+    s_.add(x);
+    if (checked_.count(x) != 0) {
+      return;
+    }
+    not_checked_.insert(x);
+  }
+  void remove(int x) {
+    s_.remove(x);
+    checked_.erase(x);
+    not_checked_.erase(x);
+  }
+  bool has_next() const {
+    auto res = !not_checked_.empty();
+    //LOG(ERROR) << res;
+    ASSERT_EQ(res, s_.has_next());
+    return res;
+  }
+  void reset_position() {
+    s_.reset_position();
+    not_checked_.insert(checked_.begin(), checked_.end());
+    checked_ = {};
+  }
+
+  T next() {
+    CHECK(has_next());
+    auto next = s_.next();
+    //LOG(ERROR) << next;
+    ASSERT_TRUE(not_checked_.count(next) != 0);
+    not_checked_.erase(next);
+    checked_.insert(next);
+    return next;
+  }
+
+  void merge(CheckedSetWithPosition &&other) {
+    if (size() < other.size()) {
+      std::swap(*this, other);
+      std::swap(this->s_, other.s_);
+    }
+    for (auto x : other.checked_) {
+      not_checked_.erase(x);
+      checked_.insert(x);
+    }
+    for (auto x : other.not_checked_) {
+      if (checked_.count(x) != 0) {
+        continue;
+      }
+      not_checked_.insert(x);
+    }
+    s_.merge(std::move(other.s_));
+  }
+  std::size_t size() const {
+    return checked_.size() + not_checked_.size();
+  }
+
+ private:
+  std::set checked_;
+  std::set not_checked_;
+  td::SetWithPosition s_;
+};
+
+template