diff --git a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h index c438ff9e3..b602acaa5 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h @@ -1,107 +1,109 @@ #pragma once #include "../CryptoTools/Persist.h" #include "entities/Draft.h" #include "entities/KeyserverInfo.h" #include "entities/Message.h" #include "entities/MessageStoreThread.h" #include "entities/OlmPersistAccount.h" #include "entities/OlmPersistSession.h" #include "entities/PersistItem.h" #include "entities/Report.h" #include "entities/Thread.h" #include "entities/UserInfo.h" #include namespace comm { /** * if any initialization/cleaning up steps are required for specific * database managers they should appear in constructors/destructors * following the RAII pattern */ class DatabaseQueryExecutor { public: virtual std::string getDraft(std::string key) const = 0; virtual std::unique_ptr getThread(std::string threadID) const = 0; virtual void updateDraft(std::string key, std::string text) const = 0; virtual bool moveDraft(std::string oldKey, std::string newKey) const = 0; virtual std::vector getAllDrafts() const = 0; virtual void removeAllDrafts() const = 0; virtual void removeDrafts(const std::vector &ids) const = 0; virtual void removeAllMessages() const = 0; virtual std::vector>> getAllMessages() const = 0; virtual void removeMessages(const std::vector &ids) const = 0; virtual void removeMessagesForThreads(const std::vector &threadIDs) const = 0; virtual void replaceMessage(const Message &message) const = 0; virtual void rekeyMessage(std::string from, std::string to) const = 0; virtual void removeAllMedia() const = 0; virtual void replaceMessageStoreThreads( const std::vector &threads) const = 0; virtual void removeMessageStoreThreads(const std::vector &ids) const = 0; virtual void removeAllMessageStoreThreads() const = 0; virtual std::vector getAllMessageStoreThreads() const = 0; virtual void removeMediaForMessages(const std::vector &msg_ids) const = 0; virtual void removeMediaForMessage(std::string msg_id) const = 0; virtual void removeMediaForThreads(const std::vector &thread_ids) const = 0; virtual void replaceMedia(const Media &media) const = 0; virtual void rekeyMediaContainers(std::string from, std::string to) const = 0; virtual std::vector getAllThreads() const = 0; virtual void removeThreads(std::vector ids) const = 0; virtual void replaceThread(const Thread &thread) const = 0; virtual void removeAllThreads() const = 0; virtual void replaceReport(const Report &report) const = 0; virtual void removeReports(const std::vector &ids) const = 0; virtual void removeAllReports() const = 0; virtual std::vector getAllReports() const = 0; virtual void setPersistStorageItem(std::string key, std::string item) const = 0; virtual void removePersistStorageItem(std::string key) const = 0; virtual std::string getPersistStorageItem(std::string key) const = 0; virtual void replaceUser(const UserInfo &user_info) const = 0; virtual void removeUsers(const std::vector &ids) const = 0; virtual void removeAllUsers() const = 0; virtual std::vector getAllUsers() const = 0; virtual void replaceKeyserver(const KeyserverInfo &keyserver_info) const = 0; virtual void removeKeyservers(const std::vector &ids) const = 0; virtual void removeAllKeyservers() const = 0; virtual std::vector getAllKeyservers() const = 0; virtual void beginTransaction() const = 0; virtual void commitTransaction() const = 0; virtual void rollbackTransaction() const = 0; virtual std::vector getOlmPersistSessionsData() const = 0; virtual std::optional getOlmPersistAccountData() const = 0; virtual void storeOlmPersistSession(const OlmPersistSession &session) const = 0; virtual void storeOlmPersistAccount(const std::string &accountData) const = 0; virtual void storeOlmPersistData(crypto::Persist persist) const = 0; virtual void setNotifyToken(std::string token) const = 0; virtual void clearNotifyToken() const = 0; virtual void setCurrentUserID(std::string userID) const = 0; virtual std::string getCurrentUserID() const = 0; virtual void setMetadata(std::string entry_name, std::string data) const = 0; virtual void clearMetadata(std::string entry_name) const = 0; virtual std::string getMetadata(std::string entry_name) const = 0; virtual void restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey) const = 0; + virtual void + restoreFromBackupLog(const std::vector &backupLog) const = 0; #ifdef EMSCRIPTEN virtual std::vector getAllThreadsWeb() const = 0; virtual void replaceThreadWeb(const WebThread &thread) const = 0; virtual std::vector getAllMessagesWeb() const = 0; virtual void replaceMessageWeb(const WebMessage &message) const = 0; virtual NullableString getOlmPersistAccountDataWeb() const = 0; #else virtual void createMainCompaction(std::string backupID) const = 0; virtual void captureBackupLogs() const = 0; #endif }; } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/NativeSQLiteConnectionManager.cpp b/native/cpp/CommonCpp/DatabaseManagers/NativeSQLiteConnectionManager.cpp index 357fe0036..7c46f7ff7 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/NativeSQLiteConnectionManager.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/NativeSQLiteConnectionManager.cpp @@ -1,235 +1,250 @@ #include "NativeSQLiteConnectionManager.h" #include "AESCrypto.h" #include "PlatformSpecificTools.h" #include #include #include #include #include namespace comm { const std::string BLOB_SERVICE_PREFIX = "comm-blob-service://"; const int IV_LENGTH = 12; const int TAG_LENGTH = 16; void NativeSQLiteConnectionManager::attachSession() { int sessionCreationResult = sqlite3session_create(dbConnection, "main", &backupLogsSession); handleSQLiteError(sessionCreationResult, "Failed to create sqlite3 session."); static const std::vector tablesToMonitor = { "drafts", "messages", "media", "threads", "message_store_threads", "reports", "keyservers", "users"}; for (const auto &table : tablesToMonitor) { int sessionAttachResult = sqlite3session_attach(backupLogsSession, table.c_str()); handleSQLiteError( sessionAttachResult, "Failed to attach sqlite3 session to " + table + " table."); } } void NativeSQLiteConnectionManager::detachSession() { if (!backupLogsSession) { return; } sqlite3session_delete(backupLogsSession); backupLogsSession = nullptr; } void NativeSQLiteConnectionManager::persistLog( std::string backupID, std::string logID, std::uint8_t *patchsetPtr, int patchsetSize, std::string encryptionKey) { std::string finalFilePath = PlatformSpecificTools::getBackupLogFilePath(backupID, logID, false); std::string tempFilePath = finalFilePath + "_tmp"; std::ofstream tempFile( tempFilePath, std::ofstream::out | std::ofstream::trunc); if (!tempFile.is_open()) { throw std::runtime_error("Failed to open temporary log file."); } std::vector logBytes(patchsetPtr, patchsetPtr + patchsetSize); std::vector encryptedLog; encryptedLog.resize(logBytes.size() + IV_LENGTH + TAG_LENGTH); std::vector encryptionKeyBytes( encryptionKey.begin(), encryptionKey.end()); AESCrypto &>::encrypt( encryptionKeyBytes, logBytes, encryptedLog); tempFile.write( reinterpret_cast(encryptedLog.data()), encryptedLog.size()); tempFile.close(); if (std::rename(tempFilePath.c_str(), finalFilePath.c_str())) { throw std::runtime_error( "Failed to rename complete log file from temporary path to target " "path."); } std::vector attachments = getAttachmentsFromLog(patchsetPtr, patchsetSize); if (attachments.empty()) { return; } std::string finalAttachmentsPath = PlatformSpecificTools::getBackupLogFilePath(backupID, logID, true); std::string tempAttachmentsPath = finalAttachmentsPath + "_tmp"; std::ofstream tempAttachmentsFile( tempAttachmentsPath, std::ofstream::out | std::ofstream::trunc); if (!tempAttachmentsFile.is_open()) { throw std::runtime_error("Failed to open temporary log attachments file."); } for (const auto &attachment : attachments) { tempAttachmentsFile << attachment << std::endl; } tempAttachmentsFile.close(); if (std::rename(tempAttachmentsPath.c_str(), finalAttachmentsPath.c_str())) { throw std::runtime_error( "Failed to rename complete log attachments file from temporary path to " "target " "path."); } } std::vector NativeSQLiteConnectionManager::getAttachmentsFromLog( std::uint8_t *patchsetPtr, int patchsetSize) { std::vector attachments; sqlite3_changeset_iter *patchsetIter; int startIterResult = sqlite3changeset_start(&patchsetIter, patchsetSize, patchsetPtr); handleSQLiteError(startIterResult, "Failed to initialize log iterator."); int nextResult; for (nextResult = sqlite3changeset_next(patchsetIter); nextResult == SQLITE_ROW; nextResult = sqlite3changeset_next(patchsetIter)) { const char *tableName; int columnsNumber; int operationType; int getOperationResult = sqlite3changeset_op( patchsetIter, &tableName, &columnsNumber, &operationType, nullptr); handleSQLiteError( getOperationResult, "Failed to extract operation from log iterator."); if (std::string(tableName) != "media") { continue; } if (operationType != SQLITE_UPDATE && operationType != SQLITE_INSERT) { continue; } sqlite3_value *uriFromMediaRow; // In "media" table "uri" column has index 3 (starting from 0) int getURIResult = sqlite3changeset_new(patchsetIter, 3, &uriFromMediaRow); handleSQLiteError( getURIResult, "Failed to extract uri value of media row from log iterator."); if (!uriFromMediaRow) { continue; } std::string uri = std::string( reinterpret_cast(sqlite3_value_text(uriFromMediaRow))); if (uri.compare(0, BLOB_SERVICE_PREFIX.size(), BLOB_SERVICE_PREFIX)) { continue; } attachments.push_back(uri.substr(BLOB_SERVICE_PREFIX.size())); } handleSQLiteError( nextResult, "Error while iterating over a log.", SQLITE_DONE); int finalizeIterResult = sqlite3changeset_finalize(patchsetIter); handleSQLiteError(finalizeIterResult, "Failed to finalize log iterator."); return attachments; } NativeSQLiteConnectionManager::NativeSQLiteConnectionManager() : backupLogsSession(nullptr) { } void NativeSQLiteConnectionManager::setLogsMonitoring(bool enabled) { if (!backupLogsSession) { return; } sqlite3session_enable(backupLogsSession, enabled); } +bool NativeSQLiteConnectionManager::getLogsMonitoring() { + if (!backupLogsSession) { + return false; + } + return sqlite3session_enable(backupLogsSession, -1); +} + void NativeSQLiteConnectionManager::initializeConnection( std::string sqliteFilePath, std::function on_db_open_callback) { SQLiteConnectionManager::initializeConnection( sqliteFilePath, on_db_open_callback); attachSession(); setLogsMonitoring(false); } void NativeSQLiteConnectionManager::closeConnection() { detachSession(); SQLiteConnectionManager::closeConnectionInternal(); } NativeSQLiteConnectionManager::~NativeSQLiteConnectionManager() { detachSession(); } bool NativeSQLiteConnectionManager::captureLogs( std::string backupID, std::string logID, std::string encryptionKey) { int patchsetSize; std::uint8_t *patchsetPtr; int getPatchsetResult = sqlite3session_patchset( backupLogsSession, &patchsetSize, (void **)&patchsetPtr); handleSQLiteError(getPatchsetResult, "Failed to get patchset from session."); if (!patchsetPtr) { return false; } if (patchsetSize == 0) { sqlite3_free(patchsetPtr); return false; } persistLog(backupID, logID, patchsetPtr, patchsetSize, encryptionKey); sqlite3_free(patchsetPtr); // The session is not "zeroed" after capturing log. // See: https://www.sqlite.org/sessionintro.html // So we need to delete and recreate session each // time we capture log. detachSession(); attachSession(); return true; } + +void NativeSQLiteConnectionManager::restoreFromBackupLog( + const std::vector &backupLog) { + bool initialEnabledValue = getLogsMonitoring(); + setLogsMonitoring(false); + SQLiteConnectionManager::restoreFromBackupLog(backupLog); + setLogsMonitoring(initialEnabledValue); +} } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/NativeSQLiteConnectionManager.h b/native/cpp/CommonCpp/DatabaseManagers/NativeSQLiteConnectionManager.h index 6ee0b7723..f8aaa36f4 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/NativeSQLiteConnectionManager.h +++ b/native/cpp/CommonCpp/DatabaseManagers/NativeSQLiteConnectionManager.h @@ -1,34 +1,37 @@ #pragma once #include "SQLiteConnectionManager.h" namespace comm { class NativeSQLiteConnectionManager : public SQLiteConnectionManager { private: sqlite3_session *backupLogsSession; void attachSession(); void detachSession(); void persistLog( std::string backupID, std::string logID, std::uint8_t *patchsetPtr, int patchsetSize, std::string encryptionKey); std::vector getAttachmentsFromLog(std::uint8_t *patchsetPtr, int patchsetSize); public: NativeSQLiteConnectionManager(); void setLogsMonitoring(bool enabled); + bool getLogsMonitoring(); void initializeConnection( std::string sqliteFilePath, std::function on_db_open_callback) override; void closeConnection() override; ~NativeSQLiteConnectionManager(); bool captureLogs( std::string backupID, std::string logID, std::string encryptionKey); + void + restoreFromBackupLog(const std::vector &backupLog) override; }; } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteConnectionManager.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteConnectionManager.cpp index e70e6db33..1dd25da99 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteConnectionManager.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteConnectionManager.cpp @@ -1,56 +1,102 @@ #include "SQLiteConnectionManager.h" +#include "Logger.h" #include #include #include #include namespace comm { SQLiteConnectionManager::SQLiteConnectionManager() : dbConnection(nullptr) { } sqlite3 *SQLiteConnectionManager::getConnection() { return dbConnection; } void SQLiteConnectionManager::handleSQLiteError( int errorCode, const std::string &errorMessagePrefix, int expectedResultCode) { if (errorCode != expectedResultCode) { throw std::runtime_error( errorMessagePrefix + " Details: " + sqlite3_errstr(errorCode)); } } void SQLiteConnectionManager::initializeConnection( std::string sqliteFilePath, std::function on_db_open_callback) { if (dbConnection) { return; } int connectResult = sqlite3_open(sqliteFilePath.c_str(), &dbConnection); handleSQLiteError(connectResult, "Failed to open database connection."); on_db_open_callback(dbConnection); } void SQLiteConnectionManager::closeConnectionInternal() { if (!dbConnection) { return; } int closeResult = sqlite3_close(dbConnection); handleSQLiteError(closeResult, "Failed to close database connection."); dbConnection = nullptr; } void SQLiteConnectionManager::closeConnection() { closeConnectionInternal(); } SQLiteConnectionManager::~SQLiteConnectionManager() { closeConnectionInternal(); } + +void SQLiteConnectionManager::restoreFromBackupLog( + const std::vector &backupLog) { + if (!dbConnection) { + throw std::runtime_error( + "Programmer error: attempt to restore from backup log but database " + "connection is not initialized."); + } + + static auto backupLogRestoreConflictHandler = + [](void *, int conflictReason, sqlite3_changeset_iter *changesetIter) { + const char *tableName; + int columnsNumber; + int operationType; + + int getOperationResult = sqlite3changeset_op( + changesetIter, &tableName, &columnsNumber, &operationType, nullptr); + handleSQLiteError( + getOperationResult, + "Failed to extract operation from log iterator."); + + std::stringstream conflictMessage; + conflictMessage << "Conflict of type " << conflictReason + << " occurred for operation of type " << operationType + << " for table " << tableName + << " during backup log application"; + Logger::log(conflictMessage.str()); + + if (operationType == SQLITE_INSERT && + conflictReason == SQLITE_CHANGESET_CONFLICT) { + return SQLITE_CHANGESET_REPLACE; + } + + return SQLITE_CHANGESET_OMIT; + }; + + int applyChangesetResult = sqlite3changeset_apply( + dbConnection, + backupLog.size(), + (void *)backupLog.data(), + nullptr, + backupLogRestoreConflictHandler, + nullptr); + handleSQLiteError(applyChangesetResult, "Failed to apply backup log."); +} } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteConnectionManager.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteConnectionManager.h index 7af7b1552..93ff79d00 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteConnectionManager.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteConnectionManager.h @@ -1,30 +1,31 @@ #pragma once #ifndef SQLITE_ENABLE_SESSION #define SQLITE_ENABLE_SESSION #endif #include #include #include namespace comm { class SQLiteConnectionManager { protected: sqlite3 *dbConnection; static void handleSQLiteError( int errorCode, const std::string &errorMessagePrefix, int expectedResultCode = SQLITE_OK); void closeConnectionInternal(); public: SQLiteConnectionManager(); sqlite3 *getConnection(); virtual void initializeConnection( std::string sqliteFilePath, std::function on_db_open_callback); virtual void closeConnection(); virtual ~SQLiteConnectionManager(); + virtual void restoreFromBackupLog(const std::vector &backupLog); }; } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp index 76454658f..be8c4414c 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp @@ -1,1902 +1,1907 @@ #include "SQLiteQueryExecutor.h" #include "Logger.h" #include "entities/EntityQueryHelpers.h" #include "entities/KeyserverInfo.h" #include "entities/Metadata.h" #include "entities/UserInfo.h" #include #include #include #ifndef EMSCRIPTEN #include "CommSecureStore.h" #include "PlatformSpecificTools.h" #include "StaffUtils.h" #endif #define ACCOUNT_ID 1 namespace comm { std::string SQLiteQueryExecutor::sqliteFilePath; std::string SQLiteQueryExecutor::encryptionKey; std::once_flag SQLiteQueryExecutor::initialized; int SQLiteQueryExecutor::sqlcipherEncryptionKeySize = 64; std::string SQLiteQueryExecutor::secureStoreEncryptionKeyID = "comm.encryptionKey"; int SQLiteQueryExecutor::backupLogsEncryptionKeySize = 32; std::string SQLiteQueryExecutor::secureStoreBackupLogsEncryptionKeyID = "comm.backupLogsEncryptionKey"; std::string SQLiteQueryExecutor::backupLogsEncryptionKey; #ifndef EMSCRIPTEN NativeSQLiteConnectionManager SQLiteQueryExecutor::connectionManager; #else SQLiteConnectionManager SQLiteQueryExecutor::connectionManager; #endif bool create_table(sqlite3 *db, std::string query, std::string tableName) { char *error; sqlite3_exec(db, query.c_str(), nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating '" << tableName << "' table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_drafts_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS drafts (threadID TEXT UNIQUE PRIMARY KEY, " "text TEXT);"; return create_table(db, query, "drafts"); } bool rename_threadID_to_key(sqlite3 *db) { sqlite3_stmt *key_column_stmt; sqlite3_prepare_v2( db, "SELECT name AS col_name FROM pragma_table_xinfo ('drafts') WHERE " "col_name='key';", -1, &key_column_stmt, nullptr); sqlite3_step(key_column_stmt); auto num_bytes = sqlite3_column_bytes(key_column_stmt, 0); sqlite3_finalize(key_column_stmt); if (num_bytes) { return true; } char *error; sqlite3_exec( db, "ALTER TABLE drafts RENAME COLUMN `threadID` TO `key`;", nullptr, nullptr, &error); if (error) { std::ostringstream stringStream; stringStream << "Error occurred renaming threadID column in drafts table " << "to key: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } return true; } bool create_persist_account_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS olm_persist_account(" "id INTEGER UNIQUE PRIMARY KEY NOT NULL, " "account_data TEXT NOT NULL);"; return create_table(db, query, "olm_persist_account"); } bool create_persist_sessions_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS olm_persist_sessions(" "target_user_id TEXT UNIQUE PRIMARY KEY NOT NULL, " "session_data TEXT NOT NULL);"; return create_table(db, query, "olm_persist_sessions"); } bool drop_messages_table(sqlite3 *db) { char *error; sqlite3_exec(db, "DROP TABLE IF EXISTS messages;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error dropping 'messages' table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool recreate_messages_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS messages ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "local_id TEXT, " "thread TEXT NOT NULL, " "user TEXT NOT NULL, " "type INTEGER NOT NULL, " "future_type INTEGER, " "content TEXT, " "time INTEGER NOT NULL);"; return create_table(db, query, "messages"); } bool create_messages_idx_thread_time(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE INDEX IF NOT EXISTS messages_idx_thread_time " "ON messages (thread, time);", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating (thread, time) index on messages table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_media_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS media ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "container TEXT NOT NULL, " "thread TEXT NOT NULL, " "uri TEXT NOT NULL, " "type TEXT NOT NULL, " "extras TEXT NOT NULL);"; return create_table(db, query, "media"); } bool create_media_idx_container(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE INDEX IF NOT EXISTS media_idx_container " "ON media (container);", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating (container) index on media table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_threads_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS threads ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "type INTEGER NOT NULL, " "name TEXT, " "description TEXT, " "color TEXT NOT NULL, " "creation_time BIGINT NOT NULL, " "parent_thread_id TEXT, " "containing_thread_id TEXT, " "community TEXT, " "members TEXT NOT NULL, " "roles TEXT NOT NULL, " "current_user TEXT NOT NULL, " "source_message_id TEXT, " "replies_count INTEGER NOT NULL);"; return create_table(db, query, "threads"); } bool update_threadID_for_pending_threads_in_drafts(sqlite3 *db) { char *error; sqlite3_exec( db, "UPDATE drafts SET key = " "REPLACE(REPLACE(REPLACE(REPLACE(key, 'type4/', '')," "'type5/', ''),'type6/', ''),'type7/', '')" "WHERE key LIKE 'pending/%'", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error update pending threadIDs on drafts table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool enable_write_ahead_logging_mode(sqlite3 *db) { char *error; sqlite3_exec(db, "PRAGMA journal_mode=wal;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error enabling write-ahead logging mode: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_metadata_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS metadata ( " "name TEXT UNIQUE PRIMARY KEY NOT NULL, " "data TEXT);"; return create_table(db, query, "metadata"); } bool add_not_null_constraint_to_drafts(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS temporary_drafts (" "key TEXT UNIQUE PRIMARY KEY NOT NULL, " "text TEXT NOT NULL);" "INSERT INTO temporary_drafts SELECT * FROM drafts " "WHERE key IS NOT NULL AND text IS NOT NULL;" "DROP TABLE drafts;" "ALTER TABLE temporary_drafts RENAME TO drafts;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding NOT NULL constraint to drafts table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool add_not_null_constraint_to_metadata(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS temporary_metadata (" "name TEXT UNIQUE PRIMARY KEY NOT NULL, " "data TEXT NOT NULL);" "INSERT INTO temporary_metadata SELECT * FROM metadata " "WHERE data IS NOT NULL;" "DROP TABLE metadata;" "ALTER TABLE temporary_metadata RENAME TO metadata;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding NOT NULL constraint to metadata table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool add_avatar_column_to_threads_table(sqlite3 *db) { char *error; sqlite3_exec( db, "ALTER TABLE threads ADD COLUMN avatar TEXT;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding avatar column to threads table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool add_pinned_count_column_to_threads(sqlite3 *db) { sqlite3_stmt *pinned_column_stmt; sqlite3_prepare_v2( db, "SELECT name AS col_name FROM pragma_table_xinfo ('threads') WHERE " "col_name='pinned_count';", -1, &pinned_column_stmt, nullptr); sqlite3_step(pinned_column_stmt); auto num_bytes = sqlite3_column_bytes(pinned_column_stmt, 0); sqlite3_finalize(pinned_column_stmt); if (num_bytes) { return true; } char *error; sqlite3_exec( db, "ALTER TABLE threads ADD COLUMN pinned_count INTEGER NOT NULL DEFAULT 0;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding pinned_count column to threads table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_message_store_threads_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS message_store_threads (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " start_reached INTEGER NOT NULL," " last_navigated_to BIGINT NOT NULL," " last_pruned BIGINT NOT NULL" ");"; return create_table(db, query, "message_store_threads"); } bool create_reports_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS reports (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " report TEXT NOT NULL" ");"; return create_table(db, query, "reports"); } bool create_persist_storage_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS persist_storage (" " key TEXT UNIQUE PRIMARY KEY NOT NULL," " item TEXT NOT NULL" ");"; return create_table(db, query, "persist_storage"); } bool recreate_message_store_threads_table(sqlite3 *db) { char *errMsg = 0; // 1. Create table without `last_navigated_to` or `last_pruned`. std::string create_new_table_query = "CREATE TABLE IF NOT EXISTS temp_message_store_threads (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " start_reached INTEGER NOT NULL" ");"; if (sqlite3_exec(db, create_new_table_query.c_str(), NULL, NULL, &errMsg) != SQLITE_OK) { Logger::log( "Error creating temp_message_store_threads: " + std::string{errMsg}); sqlite3_free(errMsg); return false; } // 2. Dump data from existing `message_store_threads` table into temp table. std::string copy_data_query = "INSERT INTO temp_message_store_threads (id, start_reached)" "SELECT id, start_reached FROM message_store_threads;"; if (sqlite3_exec(db, copy_data_query.c_str(), NULL, NULL, &errMsg) != SQLITE_OK) { Logger::log( "Error dumping data from existing message_store_threads to " "temp_message_store_threads: " + std::string{errMsg}); sqlite3_free(errMsg); return false; } // 3. Drop the existing `message_store_threads` table. std::string drop_old_table_query = "DROP TABLE message_store_threads;"; if (sqlite3_exec(db, drop_old_table_query.c_str(), NULL, NULL, &errMsg) != SQLITE_OK) { Logger::log( "Error dropping message_store_threads table: " + std::string{errMsg}); sqlite3_free(errMsg); return false; } // 4. Rename the temp table back to `message_store_threads`. std::string rename_table_query = "ALTER TABLE temp_message_store_threads RENAME TO message_store_threads;"; if (sqlite3_exec(db, rename_table_query.c_str(), NULL, NULL, &errMsg) != SQLITE_OK) { Logger::log( "Error renaming temp_message_store_threads to message_store_threads: " + std::string{errMsg}); sqlite3_free(errMsg); return false; } return true; } bool create_users_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS users (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " user_info TEXT NOT NULL" ");"; return create_table(db, query, "users"); } bool create_keyservers_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS keyservers (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " keyserver_info TEXT NOT NULL" ");"; return create_table(db, query, "keyservers"); } bool enable_rollback_journal_mode(sqlite3 *db) { char *error; sqlite3_exec(db, "PRAGMA journal_mode=DELETE;", nullptr, nullptr, &error); if (!error) { return true; } std::stringstream error_message; error_message << "Error disabling write-ahead logging mode: " << error; Logger::log(error_message.str()); sqlite3_free(error); return false; } bool create_schema(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS drafts (" " key TEXT UNIQUE PRIMARY KEY NOT NULL," " text TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS messages (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " local_id TEXT," " thread TEXT NOT NULL," " user TEXT NOT NULL," " type INTEGER NOT NULL," " future_type INTEGER," " content TEXT," " time INTEGER NOT NULL" ");" "CREATE TABLE IF NOT EXISTS olm_persist_account (" " id INTEGER UNIQUE PRIMARY KEY NOT NULL," " account_data TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS olm_persist_sessions (" " target_user_id TEXT UNIQUE PRIMARY KEY NOT NULL," " session_data TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS media (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " container TEXT NOT NULL," " thread TEXT NOT NULL," " uri TEXT NOT NULL," " type TEXT NOT NULL," " extras TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS threads (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " type INTEGER NOT NULL," " name TEXT," " description TEXT," " color TEXT NOT NULL," " creation_time BIGINT NOT NULL," " parent_thread_id TEXT," " containing_thread_id TEXT," " community TEXT," " members TEXT NOT NULL," " roles TEXT NOT NULL," " current_user TEXT NOT NULL," " source_message_id TEXT," " replies_count INTEGER NOT NULL," " avatar TEXT," " pinned_count INTEGER NOT NULL DEFAULT 0" ");" "CREATE TABLE IF NOT EXISTS metadata (" " name TEXT UNIQUE PRIMARY KEY NOT NULL," " data TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS message_store_threads (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " start_reached INTEGER NOT NULL" ");" "CREATE TABLE IF NOT EXISTS reports (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " report TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS persist_storage (" " key TEXT UNIQUE PRIMARY KEY NOT NULL," " item TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS users (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " user_info TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS keyservers (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " keyserver_info TEXT NOT NULL" ");" "CREATE INDEX IF NOT EXISTS media_idx_container" " ON media (container);" "CREATE INDEX IF NOT EXISTS messages_idx_thread_time" " ON messages (thread, time);", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating tables: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } void set_encryption_key( sqlite3 *db, const std::string &encryptionKey = SQLiteQueryExecutor::encryptionKey) { std::string set_encryption_key_query = "PRAGMA key = \"x'" + encryptionKey + "'\";"; char *error_set_key; sqlite3_exec( db, set_encryption_key_query.c_str(), nullptr, nullptr, &error_set_key); if (error_set_key) { std::ostringstream error_message; error_message << "Failed to set encryption key: " << error_set_key; throw std::system_error( ECANCELED, std::generic_category(), error_message.str()); } } int get_database_version(sqlite3 *db) { sqlite3_stmt *user_version_stmt; sqlite3_prepare_v2( db, "PRAGMA user_version;", -1, &user_version_stmt, nullptr); sqlite3_step(user_version_stmt); int current_user_version = sqlite3_column_int(user_version_stmt, 0); sqlite3_finalize(user_version_stmt); return current_user_version; } bool set_database_version(sqlite3 *db, int db_version) { std::stringstream update_version; update_version << "PRAGMA user_version=" << db_version << ";"; auto update_version_str = update_version.str(); char *error; sqlite3_exec(db, update_version_str.c_str(), nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream errorStream; errorStream << "Error setting database version to " << db_version << ": " << error; Logger::log(errorStream.str()); sqlite3_free(error); return false; } // We don't want to run `PRAGMA key = ...;` // on main web database. The context is here: // https://linear.app/comm/issue/ENG-6398/issues-with-sqlcipher-on-web void default_on_db_open_callback(sqlite3 *db) { #ifndef EMSCRIPTEN set_encryption_key(db); #endif } // This is a temporary solution. In future we want to keep // a separate table for blob hashes. Tracked on Linear: // https://linear.app/comm/issue/ENG-6261/introduce-blob-hash-table std::string blob_hash_from_blob_service_uri(const std::string &media_uri) { static const std::string blob_service_prefix = "comm-blob-service://"; return media_uri.substr(blob_service_prefix.size()); } bool file_exists(const std::string &file_path) { std::ifstream file(file_path.c_str()); return file.good(); } void attempt_delete_file( const std::string &file_path, const char *error_message) { if (std::remove(file_path.c_str())) { throw std::system_error(errno, std::generic_category(), error_message); } } void attempt_rename_file( const std::string &old_path, const std::string &new_path, const char *error_message) { if (std::rename(old_path.c_str(), new_path.c_str())) { throw std::system_error(errno, std::generic_category(), error_message); } } bool is_database_queryable( sqlite3 *db, bool use_encryption_key, const std::string &path = SQLiteQueryExecutor::sqliteFilePath, const std::string &encryptionKey = SQLiteQueryExecutor::encryptionKey) { char *err_msg; sqlite3_open(path.c_str(), &db); // According to SQLCipher documentation running some SELECT is the only way to // check for key validity if (use_encryption_key) { set_encryption_key(db, encryptionKey); } sqlite3_exec( db, "SELECT COUNT(*) FROM sqlite_master;", nullptr, nullptr, &err_msg); sqlite3_close(db); return !err_msg; } void validate_encryption() { std::string temp_encrypted_db_path = SQLiteQueryExecutor::sqliteFilePath + "_temp_encrypted"; bool temp_encrypted_exists = file_exists(temp_encrypted_db_path); bool default_location_exists = file_exists(SQLiteQueryExecutor::sqliteFilePath); if (temp_encrypted_exists && default_location_exists) { Logger::log( "Previous encryption attempt failed. Repeating encryption process from " "the beginning."); attempt_delete_file( temp_encrypted_db_path, "Failed to delete corrupted encrypted database."); } else if (temp_encrypted_exists && !default_location_exists) { Logger::log( "Moving temporary encrypted database to default location failed in " "previous encryption attempt. Repeating rename step."); attempt_rename_file( temp_encrypted_db_path, SQLiteQueryExecutor::sqliteFilePath, "Failed to move encrypted database to default location."); return; } else if (!default_location_exists) { Logger::log( "Database not present yet. It will be created encrypted under default " "path."); return; } sqlite3 *db; if (is_database_queryable(db, true)) { Logger::log( "Database exists under default path and it is correctly encrypted."); return; } if (!is_database_queryable(db, false)) { Logger::log( "Database exists but it is encrypted with key that was lost. " "Attempting database deletion. New encrypted one will be created."); attempt_delete_file( SQLiteQueryExecutor::sqliteFilePath.c_str(), "Failed to delete database encrypted with lost key."); return; } else { Logger::log( "Database exists but it is not encrypted. Attempting encryption " "process."); } sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db); std::string createEncryptedCopySQL = "ATTACH DATABASE '" + temp_encrypted_db_path + "' AS encrypted_comm " "KEY \"x'" + SQLiteQueryExecutor::encryptionKey + "'\";" "SELECT sqlcipher_export('encrypted_comm');" "DETACH DATABASE encrypted_comm;"; char *encryption_error; sqlite3_exec( db, createEncryptedCopySQL.c_str(), nullptr, nullptr, &encryption_error); if (encryption_error) { throw std::system_error( ECANCELED, std::generic_category(), "Failed to create encrypted copy of the original database."); } sqlite3_close(db); attempt_delete_file( SQLiteQueryExecutor::sqliteFilePath, "Failed to delete unencrypted database."); attempt_rename_file( temp_encrypted_db_path, SQLiteQueryExecutor::sqliteFilePath, "Failed to move encrypted database to default location."); Logger::log("Encryption completed successfully."); } typedef bool ShouldBeInTransaction; typedef std::function MigrateFunction; typedef std::pair SQLiteMigration; std::vector> migrations{ {{1, {create_drafts_table, true}}, {2, {rename_threadID_to_key, true}}, {4, {create_persist_account_table, true}}, {5, {create_persist_sessions_table, true}}, {15, {create_media_table, true}}, {16, {drop_messages_table, true}}, {17, {recreate_messages_table, true}}, {18, {create_messages_idx_thread_time, true}}, {19, {create_media_idx_container, true}}, {20, {create_threads_table, true}}, {21, {update_threadID_for_pending_threads_in_drafts, true}}, {22, {enable_write_ahead_logging_mode, false}}, {23, {create_metadata_table, true}}, {24, {add_not_null_constraint_to_drafts, true}}, {25, {add_not_null_constraint_to_metadata, true}}, {26, {add_avatar_column_to_threads_table, true}}, {27, {add_pinned_count_column_to_threads, true}}, {28, {create_message_store_threads_table, true}}, {29, {create_reports_table, true}}, {30, {create_persist_storage_table, true}}, {31, {recreate_message_store_threads_table, true}}, {32, {create_users_table, true}}, {33, {create_keyservers_table, true}}, {34, {enable_rollback_journal_mode, false}}}}; enum class MigrationResult { SUCCESS, FAILURE, NOT_APPLIED }; MigrationResult applyMigrationWithTransaction( sqlite3 *db, const MigrateFunction &migrate, int index) { sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); auto db_version = get_database_version(db); if (index <= db_version) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::NOT_APPLIED; } auto rc = migrate(db); if (!rc) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::FAILURE; } auto database_version_set = set_database_version(db, index); if (!database_version_set) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::FAILURE; } sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); return MigrationResult::SUCCESS; } MigrationResult applyMigrationWithoutTransaction( sqlite3 *db, const MigrateFunction &migrate, int index) { auto db_version = get_database_version(db); if (index <= db_version) { return MigrationResult::NOT_APPLIED; } auto rc = migrate(db); if (!rc) { return MigrationResult::FAILURE; } sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); auto inner_db_version = get_database_version(db); if (index <= inner_db_version) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::NOT_APPLIED; } auto database_version_set = set_database_version(db, index); if (!database_version_set) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::FAILURE; } sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); return MigrationResult::SUCCESS; } bool set_up_database(sqlite3 *db) { sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); auto db_version = get_database_version(db); auto latest_version = migrations.back().first; if (db_version == latest_version) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return true; } if (db_version != 0 || !create_schema(db) || !set_database_version(db, latest_version)) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); return true; } void SQLiteQueryExecutor::migrate() { // We don't want to run `PRAGMA key = ...;` // on main web database. The context is here: // https://linear.app/comm/issue/ENG-6398/issues-with-sqlcipher-on-web #ifndef EMSCRIPTEN validate_encryption(); #endif sqlite3 *db; sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db); default_on_db_open_callback(db); std::stringstream db_path; db_path << "db path: " << SQLiteQueryExecutor::sqliteFilePath.c_str() << std::endl; Logger::log(db_path.str()); auto db_version = get_database_version(db); std::stringstream version_msg; version_msg << "db version: " << db_version << std::endl; Logger::log(version_msg.str()); if (db_version == 0) { auto db_created = set_up_database(db); if (!db_created) { sqlite3_close(db); Logger::log("Database structure creation error."); throw std::runtime_error("Database structure creation error"); } Logger::log("Database structure created."); sqlite3_close(db); return; } for (const auto &[idx, migration] : migrations) { const auto &[applyMigration, shouldBeInTransaction] = migration; MigrationResult migrationResult; if (shouldBeInTransaction) { migrationResult = applyMigrationWithTransaction(db, applyMigration, idx); } else { migrationResult = applyMigrationWithoutTransaction(db, applyMigration, idx); } if (migrationResult == MigrationResult::NOT_APPLIED) { continue; } std::stringstream migration_msg; if (migrationResult == MigrationResult::FAILURE) { migration_msg << "migration " << idx << " failed." << std::endl; Logger::log(migration_msg.str()); sqlite3_close(db); throw std::runtime_error(migration_msg.str()); } if (migrationResult == MigrationResult::SUCCESS) { migration_msg << "migration " << idx << " succeeded." << std::endl; Logger::log(migration_msg.str()); } } sqlite3_close(db); } SQLiteQueryExecutor::SQLiteQueryExecutor() { SQLiteQueryExecutor::migrate(); #ifndef EMSCRIPTEN std::string currentBackupID = this->getMetadata("backupID"); if (!StaffUtils::isStaffRelease() || !currentBackupID.size()) { return; } SQLiteQueryExecutor::connectionManager.setLogsMonitoring(true); #endif } SQLiteQueryExecutor::SQLiteQueryExecutor(std::string sqliteFilePath) { SQLiteQueryExecutor::sqliteFilePath = sqliteFilePath; SQLiteQueryExecutor::migrate(); } sqlite3 *SQLiteQueryExecutor::getConnection() { if (SQLiteQueryExecutor::connectionManager.getConnection()) { return SQLiteQueryExecutor::connectionManager.getConnection(); } SQLiteQueryExecutor::connectionManager.initializeConnection( SQLiteQueryExecutor::sqliteFilePath, default_on_db_open_callback); return SQLiteQueryExecutor::connectionManager.getConnection(); } void SQLiteQueryExecutor::closeConnection() { SQLiteQueryExecutor::connectionManager.closeConnection(); } SQLiteQueryExecutor::~SQLiteQueryExecutor() { SQLiteQueryExecutor::closeConnection(); } std::string SQLiteQueryExecutor::getDraft(std::string key) const { static std::string getDraftByPrimaryKeySQL = "SELECT * " "FROM drafts " "WHERE key = ?;"; std::unique_ptr draft = getEntityByPrimaryKey( SQLiteQueryExecutor::getConnection(), getDraftByPrimaryKeySQL, key); return (draft == nullptr) ? "" : draft->text; } std::unique_ptr SQLiteQueryExecutor::getThread(std::string threadID) const { static std::string getThreadByPrimaryKeySQL = "SELECT * " "FROM threads " "WHERE id = ?;"; return getEntityByPrimaryKey( SQLiteQueryExecutor::getConnection(), getThreadByPrimaryKeySQL, threadID); } void SQLiteQueryExecutor::updateDraft(std::string key, std::string text) const { static std::string replaceDraftSQL = "REPLACE INTO drafts (key, text) " "VALUES (?, ?);"; Draft draft = {key, text}; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceDraftSQL, draft); } bool SQLiteQueryExecutor::moveDraft(std::string oldKey, std::string newKey) const { std::string draftText = this->getDraft(oldKey); if (!draftText.size()) { return false; } static std::string rekeyDraftSQL = "UPDATE OR REPLACE drafts " "SET key = ? " "WHERE key = ?;"; rekeyAllEntities( SQLiteQueryExecutor::getConnection(), rekeyDraftSQL, oldKey, newKey); return true; } std::vector SQLiteQueryExecutor::getAllDrafts() const { static std::string getAllDraftsSQL = "SELECT * " "FROM drafts;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllDraftsSQL); } void SQLiteQueryExecutor::removeAllDrafts() const { static std::string removeAllDraftsSQL = "DELETE FROM drafts;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllDraftsSQL); } void SQLiteQueryExecutor::removeDrafts( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeDraftsByKeysSQLStream; removeDraftsByKeysSQLStream << "DELETE FROM drafts " "WHERE key IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeDraftsByKeysSQLStream.str(), ids); } void SQLiteQueryExecutor::removeAllMessages() const { static std::string removeAllMessagesSQL = "DELETE FROM messages;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllMessagesSQL); } std::vector>> SQLiteQueryExecutor::getAllMessages() const { static std::string getAllMessagesSQL = "SELECT * " "FROM messages " "LEFT JOIN media " " ON messages.id = media.container " "ORDER BY messages.id;"; SQLiteStatementWrapper preparedSQL( SQLiteQueryExecutor::getConnection(), getAllMessagesSQL, "Failed to retrieve all messages."); std::string prevMsgIdx{}; std::vector>> allMessages; for (int stepResult = sqlite3_step(preparedSQL); stepResult == SQLITE_ROW; stepResult = sqlite3_step(preparedSQL)) { Message message = Message::fromSQLResult(preparedSQL, 0); if (message.id == prevMsgIdx) { allMessages.back().second.push_back(Media::fromSQLResult(preparedSQL, 8)); } else { prevMsgIdx = message.id; std::vector mediaForMsg; if (sqlite3_column_type(preparedSQL, 8) != SQLITE_NULL) { mediaForMsg.push_back(Media::fromSQLResult(preparedSQL, 8)); } allMessages.push_back(std::make_pair(std::move(message), mediaForMsg)); } } return allMessages; } void SQLiteQueryExecutor::removeMessages( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeMessagesByKeysSQLStream; removeMessagesByKeysSQLStream << "DELETE FROM messages " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMessagesByKeysSQLStream.str(), ids); } void SQLiteQueryExecutor::removeMessagesForThreads( const std::vector &threadIDs) const { if (!threadIDs.size()) { return; } std::stringstream removeMessagesByKeysSQLStream; removeMessagesByKeysSQLStream << "DELETE FROM messages " "WHERE thread IN " << getSQLStatementArray(threadIDs.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMessagesByKeysSQLStream.str(), threadIDs); } void SQLiteQueryExecutor::replaceMessage(const Message &message) const { static std::string replaceMessageSQL = "REPLACE INTO messages " "(id, local_id, thread, user, type, future_type, content, time) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceMessageSQL, message); } void SQLiteQueryExecutor::rekeyMessage(std::string from, std::string to) const { static std::string rekeyMessageSQL = "UPDATE OR REPLACE messages " "SET id = ? " "WHERE id = ?"; rekeyAllEntities( SQLiteQueryExecutor::getConnection(), rekeyMessageSQL, from, to); } void SQLiteQueryExecutor::removeAllMedia() const { static std::string removeAllMediaSQL = "DELETE FROM media;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllMediaSQL); } void SQLiteQueryExecutor::removeMediaForMessages( const std::vector &msg_ids) const { if (!msg_ids.size()) { return; } std::stringstream removeMediaByKeysSQLStream; removeMediaByKeysSQLStream << "DELETE FROM media " "WHERE container IN " << getSQLStatementArray(msg_ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMediaByKeysSQLStream.str(), msg_ids); } void SQLiteQueryExecutor::removeMediaForMessage(std::string msg_id) const { static std::string removeMediaByKeySQL = "DELETE FROM media " "WHERE container IN (?);"; std::vector keys = {msg_id}; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMediaByKeySQL, keys); } void SQLiteQueryExecutor::removeMediaForThreads( const std::vector &thread_ids) const { if (!thread_ids.size()) { return; } std::stringstream removeMediaByKeysSQLStream; removeMediaByKeysSQLStream << "DELETE FROM media " "WHERE thread IN " << getSQLStatementArray(thread_ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMediaByKeysSQLStream.str(), thread_ids); } void SQLiteQueryExecutor::replaceMedia(const Media &media) const { static std::string replaceMediaSQL = "REPLACE INTO media " "(id, container, thread, uri, type, extras) " "VALUES (?, ?, ?, ?, ?, ?)"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceMediaSQL, media); } void SQLiteQueryExecutor::rekeyMediaContainers(std::string from, std::string to) const { static std::string rekeyMediaContainersSQL = "UPDATE media SET container = ? WHERE container = ?;"; rekeyAllEntities( SQLiteQueryExecutor::getConnection(), rekeyMediaContainersSQL, from, to); } void SQLiteQueryExecutor::replaceMessageStoreThreads( const std::vector &threads) const { static std::string replaceMessageStoreThreadSQL = "REPLACE INTO message_store_threads " "(id, start_reached) " "VALUES (?, ?);"; for (auto &thread : threads) { replaceEntity( SQLiteQueryExecutor::getConnection(), replaceMessageStoreThreadSQL, thread); } } void SQLiteQueryExecutor::removeAllMessageStoreThreads() const { static std::string removeAllMessageStoreThreadsSQL = "DELETE FROM message_store_threads;"; removeAllEntities( SQLiteQueryExecutor::getConnection(), removeAllMessageStoreThreadsSQL); } void SQLiteQueryExecutor::removeMessageStoreThreads( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeMessageStoreThreadsByKeysSQLStream; removeMessageStoreThreadsByKeysSQLStream << "DELETE FROM message_store_threads " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMessageStoreThreadsByKeysSQLStream.str(), ids); } std::vector SQLiteQueryExecutor::getAllMessageStoreThreads() const { static std::string getAllMessageStoreThreadsSQL = "SELECT * " "FROM message_store_threads;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllMessageStoreThreadsSQL); } std::vector SQLiteQueryExecutor::getAllThreads() const { static std::string getAllThreadsSQL = "SELECT * " "FROM threads;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllThreadsSQL); }; void SQLiteQueryExecutor::removeThreads(std::vector ids) const { if (!ids.size()) { return; } std::stringstream removeThreadsByKeysSQLStream; removeThreadsByKeysSQLStream << "DELETE FROM threads " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeThreadsByKeysSQLStream.str(), ids); }; void SQLiteQueryExecutor::replaceThread(const Thread &thread) const { static std::string replaceThreadSQL = "REPLACE INTO threads (" " id, type, name, description, color, creation_time, parent_thread_id," " containing_thread_id, community, members, roles, current_user," " source_message_id, replies_count, avatar, pinned_count) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceThreadSQL, thread); }; void SQLiteQueryExecutor::removeAllThreads() const { static std::string removeAllThreadsSQL = "DELETE FROM threads;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllThreadsSQL); }; void SQLiteQueryExecutor::replaceReport(const Report &report) const { static std::string replaceReportSQL = "REPLACE INTO reports (id, report) " "VALUES (?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceReportSQL, report); } void SQLiteQueryExecutor::removeAllReports() const { static std::string removeAllReportsSQL = "DELETE FROM reports;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllReportsSQL); } void SQLiteQueryExecutor::removeReports( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeReportsByKeysSQLStream; removeReportsByKeysSQLStream << "DELETE FROM reports " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeReportsByKeysSQLStream.str(), ids); } std::vector SQLiteQueryExecutor::getAllReports() const { static std::string getAllReportsSQL = "SELECT * " "FROM reports;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllReportsSQL); } void SQLiteQueryExecutor::setPersistStorageItem( std::string key, std::string item) const { static std::string replacePersistStorageItemSQL = "REPLACE INTO persist_storage (key, item) " "VALUES (?, ?);"; PersistItem entry{ key, item, }; replaceEntity( SQLiteQueryExecutor::getConnection(), replacePersistStorageItemSQL, entry); } void SQLiteQueryExecutor::removePersistStorageItem(std::string key) const { static std::string removePersistStorageItemByKeySQL = "DELETE FROM persist_storage " "WHERE key IN (?);"; std::vector keys = {key}; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removePersistStorageItemByKeySQL, keys); } std::string SQLiteQueryExecutor::getPersistStorageItem(std::string key) const { static std::string getPersistStorageItemByPrimaryKeySQL = "SELECT * " "FROM persist_storage " "WHERE key = ?;"; std::unique_ptr entry = getEntityByPrimaryKey( SQLiteQueryExecutor::getConnection(), getPersistStorageItemByPrimaryKeySQL, key); return (entry == nullptr) ? "" : entry->item; } void SQLiteQueryExecutor::replaceUser(const UserInfo &user_info) const { static std::string replaceUserSQL = "REPLACE INTO users (id, user_info) " "VALUES (?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceUserSQL, user_info); } void SQLiteQueryExecutor::removeAllUsers() const { static std::string removeAllUsersSQL = "DELETE FROM users;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllUsersSQL); } void SQLiteQueryExecutor::removeUsers( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeUsersByKeysSQLStream; removeUsersByKeysSQLStream << "DELETE FROM users " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeUsersByKeysSQLStream.str(), ids); } void SQLiteQueryExecutor::replaceKeyserver( const KeyserverInfo &keyserver_info) const { static std::string replaceKeyserverSQL = "REPLACE INTO keyservers (id, keyserver_info) " "VALUES (?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceKeyserverSQL, keyserver_info); } void SQLiteQueryExecutor::removeAllKeyservers() const { static std::string removeAllKeyserversSQL = "DELETE FROM keyservers;"; removeAllEntities( SQLiteQueryExecutor::getConnection(), removeAllKeyserversSQL); } void SQLiteQueryExecutor::removeKeyservers( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeKeyserversByKeysSQLStream; removeKeyserversByKeysSQLStream << "DELETE FROM keyservers " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeKeyserversByKeysSQLStream.str(), ids); } std::vector SQLiteQueryExecutor::getAllKeyservers() const { static std::string getAllKeyserversSQL = "SELECT * " "FROM keyservers;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllKeyserversSQL); } std::vector SQLiteQueryExecutor::getAllUsers() const { static std::string getAllUsersSQL = "SELECT * " "FROM users;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllUsersSQL); } void SQLiteQueryExecutor::beginTransaction() const { executeQuery(SQLiteQueryExecutor::getConnection(), "BEGIN TRANSACTION;"); } void SQLiteQueryExecutor::commitTransaction() const { executeQuery(SQLiteQueryExecutor::getConnection(), "COMMIT;"); } void SQLiteQueryExecutor::rollbackTransaction() const { executeQuery(SQLiteQueryExecutor::getConnection(), "ROLLBACK;"); } std::vector SQLiteQueryExecutor::getOlmPersistSessionsData() const { static std::string getAllOlmPersistSessionsSQL = "SELECT * " "FROM olm_persist_sessions;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllOlmPersistSessionsSQL); } std::optional SQLiteQueryExecutor::getOlmPersistAccountData() const { static std::string getAllOlmPersistAccountSQL = "SELECT * " "FROM olm_persist_account;"; std::vector result = getAllEntities( SQLiteQueryExecutor::getConnection(), getAllOlmPersistAccountSQL); if (result.size() > 1) { throw std::system_error( ECANCELED, std::generic_category(), "Multiple records found for the olm_persist_account table"); } return (result.size() == 0) ? std::nullopt : std::optional(result[0].account_data); } void SQLiteQueryExecutor::storeOlmPersistAccount( const std::string &accountData) const { static std::string replaceOlmPersistAccountSQL = "REPLACE INTO olm_persist_account (id, account_data) " "VALUES (?, ?);"; OlmPersistAccount persistAccount = {ACCOUNT_ID, accountData}; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceOlmPersistAccountSQL, persistAccount); } void SQLiteQueryExecutor::storeOlmPersistSession( const OlmPersistSession &session) const { static std::string replaceOlmPersistSessionSQL = "REPLACE INTO olm_persist_sessions (target_user_id, session_data) " "VALUES (?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceOlmPersistSessionSQL, session); } void SQLiteQueryExecutor::storeOlmPersistData(crypto::Persist persist) const { std::string accountData = std::string(persist.account.begin(), persist.account.end()); for (auto it = persist.sessions.begin(); it != persist.sessions.end(); it++) { OlmPersistSession persistSession = { it->first, std::string(it->second.begin(), it->second.end())}; this->storeOlmPersistSession(persistSession); } } void SQLiteQueryExecutor::setNotifyToken(std::string token) const { this->setMetadata("notify_token", token); } void SQLiteQueryExecutor::clearNotifyToken() const { this->clearMetadata("notify_token"); } void SQLiteQueryExecutor::setCurrentUserID(std::string userID) const { this->setMetadata("current_user_id", userID); } std::string SQLiteQueryExecutor::getCurrentUserID() const { return this->getMetadata("current_user_id"); } void SQLiteQueryExecutor::setMetadata(std::string entry_name, std::string data) const { std::string replaceMetadataSQL = "REPLACE INTO metadata (name, data) " "VALUES (?, ?);"; Metadata entry{ entry_name, data, }; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceMetadataSQL, entry); } void SQLiteQueryExecutor::clearMetadata(std::string entry_name) const { static std::string removeMetadataByKeySQL = "DELETE FROM metadata " "WHERE name IN (?);"; std::vector keys = {entry_name}; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMetadataByKeySQL, keys); } std::string SQLiteQueryExecutor::getMetadata(std::string entry_name) const { std::string getMetadataByPrimaryKeySQL = "SELECT * " "FROM metadata " "WHERE name = ?;"; std::unique_ptr entry = getEntityByPrimaryKey( SQLiteQueryExecutor::getConnection(), getMetadataByPrimaryKeySQL, entry_name); return (entry == nullptr) ? "" : entry->data; } #ifdef EMSCRIPTEN std::vector SQLiteQueryExecutor::getAllThreadsWeb() const { auto threads = this->getAllThreads(); std::vector webThreads; webThreads.reserve(threads.size()); for (const auto &thread : threads) { webThreads.emplace_back(thread); } return webThreads; }; void SQLiteQueryExecutor::replaceThreadWeb(const WebThread &thread) const { this->replaceThread(thread.toThread()); }; std::vector SQLiteQueryExecutor::getAllMessagesWeb() const { auto allMessages = this->getAllMessages(); std::vector allMessageWithMedias; for (auto &messageWitMedia : allMessages) { allMessageWithMedias.push_back( {std::move(messageWitMedia.first), messageWitMedia.second}); } return allMessageWithMedias; } void SQLiteQueryExecutor::replaceMessageWeb(const WebMessage &message) const { this->replaceMessage(message.toMessage()); }; NullableString SQLiteQueryExecutor::getOlmPersistAccountDataWeb() const { std::optional accountData = this->getOlmPersistAccountData(); if (!accountData.has_value()) { return NullableString(); } return std::make_unique(accountData.value()); } #else void SQLiteQueryExecutor::clearSensitiveData() { SQLiteQueryExecutor::closeConnection(); if (file_exists(SQLiteQueryExecutor::sqliteFilePath) && std::remove(SQLiteQueryExecutor::sqliteFilePath.c_str())) { std::ostringstream errorStream; errorStream << "Failed to delete database file. Details: " << strerror(errno); throw std::system_error(errno, std::generic_category(), errorStream.str()); } SQLiteQueryExecutor::assign_encryption_key(); SQLiteQueryExecutor::migrate(); } void SQLiteQueryExecutor::initialize(std::string &databasePath) { std::call_once(SQLiteQueryExecutor::initialized, [&databasePath]() { SQLiteQueryExecutor::sqliteFilePath = databasePath; folly::Optional maybeEncryptionKey = CommSecureStore::get(SQLiteQueryExecutor::secureStoreEncryptionKeyID); folly::Optional maybeBackupLogsEncryptionKey = CommSecureStore::get( SQLiteQueryExecutor::secureStoreBackupLogsEncryptionKeyID); if (file_exists(databasePath) && maybeEncryptionKey && maybeBackupLogsEncryptionKey) { SQLiteQueryExecutor::encryptionKey = maybeEncryptionKey.value(); SQLiteQueryExecutor::backupLogsEncryptionKey = maybeBackupLogsEncryptionKey.value(); return; } SQLiteQueryExecutor::assign_encryption_key(); }); } void SQLiteQueryExecutor::createMainCompaction(std::string backupID) const { std::string finalBackupPath = PlatformSpecificTools::getBackupFilePath(backupID, false); std::string finalAttachmentsPath = PlatformSpecificTools::getBackupFilePath(backupID, true); std::string tempBackupPath = finalBackupPath + "_tmp"; std::string tempAttachmentsPath = finalAttachmentsPath + "_tmp"; if (file_exists(tempBackupPath)) { Logger::log( "Attempting to delete temporary backup file from previous backup " "attempt."); attempt_delete_file( tempBackupPath, "Failed to delete temporary backup file from previous backup attempt."); } if (file_exists(tempAttachmentsPath)) { Logger::log( "Attempting to delete temporary attachments file from previous backup " "attempt."); attempt_delete_file( tempAttachmentsPath, "Failed to delete temporary attachments file from previous backup " "attempt."); } sqlite3 *backupDB; sqlite3_open(tempBackupPath.c_str(), &backupDB); set_encryption_key(backupDB); sqlite3_backup *backupObj = sqlite3_backup_init( backupDB, "main", SQLiteQueryExecutor::getConnection(), "main"); if (!backupObj) { std::stringstream error_message; error_message << "Failed to init backup for main compaction. Details: " << sqlite3_errmsg(backupDB) << std::endl; sqlite3_close(backupDB); throw std::runtime_error(error_message.str()); } int backupResult = sqlite3_backup_step(backupObj, -1); sqlite3_backup_finish(backupObj); if (backupResult == SQLITE_BUSY || backupResult == SQLITE_LOCKED) { sqlite3_close(backupDB); throw std::runtime_error( "Programmer error. Database in transaction during backup attempt."); } else if (backupResult != SQLITE_DONE) { sqlite3_close(backupDB); std::stringstream error_message; error_message << "Failed to create database backup. Details: " << sqlite3_errstr(backupResult); throw std::runtime_error(error_message.str()); } executeQuery(backupDB, "VACUUM;"); sqlite3_close(backupDB); attempt_rename_file( tempBackupPath, finalBackupPath, "Failed to rename complete temporary backup file to final backup file."); std::ofstream tempAttachmentsFile(tempAttachmentsPath); if (!tempAttachmentsFile.is_open()) { throw std::runtime_error( "Unable to create attachments file for backup id: " + backupID); } std::string getAllBlobServiceMediaSQL = "SELECT * FROM media WHERE uri LIKE 'comm-blob-service://%';"; std::vector blobServiceMedia = getAllEntities( SQLiteQueryExecutor::getConnection(), getAllBlobServiceMediaSQL); for (const auto &media : blobServiceMedia) { std::string blobServiceURI = media.uri; std::string blobHash = blob_hash_from_blob_service_uri(blobServiceURI); tempAttachmentsFile << blobHash << "\n"; } tempAttachmentsFile.close(); attempt_rename_file( tempAttachmentsPath, finalAttachmentsPath, "Failed to rename complete temporary attachments file to final " "attachments file."); this->setMetadata("backupID", backupID); this->clearMetadata("logID"); if (StaffUtils::isStaffRelease()) { SQLiteQueryExecutor::connectionManager.setLogsMonitoring(true); } } void SQLiteQueryExecutor::assign_encryption_key() { std::string encryptionKey = comm::crypto::Tools::generateRandomHexString( SQLiteQueryExecutor::sqlcipherEncryptionKeySize); std::string backupLogsEncryptionKey = comm::crypto::Tools::generateRandomHexString( SQLiteQueryExecutor::backupLogsEncryptionKeySize); CommSecureStore::set( SQLiteQueryExecutor::secureStoreEncryptionKeyID, encryptionKey); CommSecureStore::set( SQLiteQueryExecutor::secureStoreBackupLogsEncryptionKeyID, backupLogsEncryptionKey); SQLiteQueryExecutor::encryptionKey = encryptionKey; SQLiteQueryExecutor::backupLogsEncryptionKey = backupLogsEncryptionKey; } void SQLiteQueryExecutor::captureBackupLogs() const { std::string backupID = this->getMetadata("backupID"); if (!backupID.size()) { return; } std::string logID = this->getMetadata("logID"); if (!logID.size()) { logID = "0"; } bool newLogCreated = SQLiteQueryExecutor::connectionManager.captureLogs( backupID, logID, SQLiteQueryExecutor::backupLogsEncryptionKey); if (!newLogCreated) { return; } this->setMetadata("logID", std::to_string(std::stoi(logID) + 1)); } #endif void SQLiteQueryExecutor::restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey) const { if (!file_exists(mainCompactionPath)) { throw std::runtime_error("Restore attempt but backup file does not exist."); } sqlite3 *backupDB; if (!is_database_queryable( backupDB, true, mainCompactionPath, mainCompactionEncryptionKey)) { throw std::runtime_error("Backup file or encryption key corrupted."); } // We don't want to run `PRAGMA key = ...;` // on main web database. The context is here: // https://linear.app/comm/issue/ENG-6398/issues-with-sqlcipher-on-web #ifdef EMSCRIPTEN std::string plaintextBackupPath = mainCompactionPath + "_plaintext"; if (file_exists(plaintextBackupPath)) { attempt_delete_file( plaintextBackupPath, "Failed to delete plaintext backup file from previous backup attempt."); } std::string plaintextMigrationDBQuery = "PRAGMA key = \"x'" + mainCompactionEncryptionKey + "'\";" "ATTACH DATABASE '" + plaintextBackupPath + "' AS plaintext KEY '';" "SELECT sqlcipher_export('plaintext');" "DETACH DATABASE plaintext;"; sqlite3_open(mainCompactionPath.c_str(), &backupDB); char *plaintextMigrationErr; sqlite3_exec( backupDB, plaintextMigrationDBQuery.c_str(), nullptr, nullptr, &plaintextMigrationErr); sqlite3_close(backupDB); if (plaintextMigrationErr) { std::stringstream error_message; error_message << "Failed to migrate backup SQLCipher file to plaintext " "SQLite file. Details" << plaintextMigrationErr << std::endl; std::string error_message_str = error_message.str(); sqlite3_free(plaintextMigrationErr); throw std::runtime_error(error_message_str); } sqlite3_open(plaintextBackupPath.c_str(), &backupDB); #else sqlite3_open(mainCompactionPath.c_str(), &backupDB); set_encryption_key(backupDB, mainCompactionEncryptionKey); #endif sqlite3_backup *backupObj = sqlite3_backup_init( SQLiteQueryExecutor::getConnection(), "main", backupDB, "main"); if (!backupObj) { std::stringstream error_message; error_message << "Failed to init backup for main compaction. Details: " << sqlite3_errmsg(SQLiteQueryExecutor::getConnection()) << std::endl; sqlite3_close(backupDB); throw std::runtime_error(error_message.str()); } int backupResult = sqlite3_backup_step(backupObj, -1); sqlite3_backup_finish(backupObj); sqlite3_close(backupDB); if (backupResult == SQLITE_BUSY || backupResult == SQLITE_LOCKED) { throw std::runtime_error( "Programmer error. Database in transaction during restore attempt."); } else if (backupResult != SQLITE_DONE) { std::stringstream error_message; error_message << "Failed to restore database from backup. Details: " << sqlite3_errstr(backupResult); throw std::runtime_error(error_message.str()); } #ifdef EMSCRIPTEN attempt_delete_file( plaintextBackupPath, "Failed to delete plaintext compaction file after successful restore."); #endif attempt_delete_file( mainCompactionPath, "Failed to delete main compaction file after successful restore."); } +void SQLiteQueryExecutor::restoreFromBackupLog( + const std::vector &backupLog) const { + SQLiteQueryExecutor::connectionManager.restoreFromBackupLog(backupLog); +} + } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h index 9c666077a..bf7f73d71 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -1,122 +1,124 @@ #pragma once #include "../CryptoTools/Persist.h" #include "DatabaseQueryExecutor.h" #include "NativeSQLiteConnectionManager.h" #include "entities/Draft.h" #include "entities/KeyserverInfo.h" #include "entities/UserInfo.h" #include #include namespace comm { class SQLiteQueryExecutor : public DatabaseQueryExecutor { static void migrate(); static sqlite3 *getConnection(); static void closeConnection(); static std::once_flag initialized; static int sqlcipherEncryptionKeySize; static std::string secureStoreEncryptionKeyID; static int backupLogsEncryptionKeySize; static std::string secureStoreBackupLogsEncryptionKeyID; static std::string backupLogsEncryptionKey; #ifndef EMSCRIPTEN static NativeSQLiteConnectionManager connectionManager; static void assign_encryption_key(); #else static SQLiteConnectionManager connectionManager; #endif public: static std::string sqliteFilePath; static std::string encryptionKey; SQLiteQueryExecutor(); ~SQLiteQueryExecutor(); SQLiteQueryExecutor(std::string sqliteFilePath); std::unique_ptr getThread(std::string threadID) const override; std::string getDraft(std::string key) const override; void updateDraft(std::string key, std::string text) const override; bool moveDraft(std::string oldKey, std::string newKey) const override; std::vector getAllDrafts() const override; void removeAllDrafts() const override; void removeDrafts(const std::vector &ids) const override; void removeAllMessages() const override; std::vector>> getAllMessages() const override; void removeMessages(const std::vector &ids) const override; void removeMessagesForThreads( const std::vector &threadIDs) const override; void replaceMessage(const Message &message) const override; void rekeyMessage(std::string from, std::string to) const override; void replaceMessageStoreThreads( const std::vector &threads) const override; void removeMessageStoreThreads(const std::vector &ids) const override; void removeAllMessageStoreThreads() const override; std::vector getAllMessageStoreThreads() const override; void removeAllMedia() const override; void removeMediaForMessages( const std::vector &msg_ids) const override; void removeMediaForMessage(std::string msg_id) const override; void removeMediaForThreads( const std::vector &thread_ids) const override; void replaceMedia(const Media &media) const override; void rekeyMediaContainers(std::string from, std::string to) const override; std::vector getAllThreads() const override; void removeThreads(std::vector ids) const override; void replaceThread(const Thread &thread) const override; void removeAllThreads() const override; void replaceReport(const Report &report) const override; void removeReports(const std::vector &ids) const override; void removeAllReports() const override; std::vector getAllReports() const override; void setPersistStorageItem(std::string key, std::string item) const override; void removePersistStorageItem(std::string key) const override; std::string getPersistStorageItem(std::string key) const override; void replaceUser(const UserInfo &user_info) const override; void removeUsers(const std::vector &ids) const override; void removeAllUsers() const override; std::vector getAllUsers() const override; void replaceKeyserver(const KeyserverInfo &keyserver_info) const override; void removeKeyservers(const std::vector &ids) const override; void removeAllKeyservers() const override; std::vector getAllKeyservers() const override; void beginTransaction() const override; void commitTransaction() const override; void rollbackTransaction() const override; std::vector getOlmPersistSessionsData() const override; std::optional getOlmPersistAccountData() const override; void storeOlmPersistSession(const OlmPersistSession &session) const override; void storeOlmPersistAccount(const std::string &accountData) const override; void storeOlmPersistData(crypto::Persist persist) const override; void setNotifyToken(std::string token) const override; void clearNotifyToken() const override; void setCurrentUserID(std::string userID) const override; std::string getCurrentUserID() const override; void setMetadata(std::string entry_name, std::string data) const override; void clearMetadata(std::string entry_name) const override; std::string getMetadata(std::string entry_name) const override; void restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey) const override; + void restoreFromBackupLog( + const std::vector &backupLog) const override; #ifdef EMSCRIPTEN std::vector getAllThreadsWeb() const override; void replaceThreadWeb(const WebThread &thread) const override; std::vector getAllMessagesWeb() const override; void replaceMessageWeb(const WebMessage &message) const override; NullableString getOlmPersistAccountDataWeb() const override; #else static void clearSensitiveData(); static void initialize(std::string &databasePath); void createMainCompaction(std::string backupID) const override; void captureBackupLogs() const override; #endif }; } // namespace comm diff --git a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.cpp b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.cpp index d60ddbf33..c199adf7b 100644 --- a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.cpp +++ b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.cpp @@ -1,39 +1,54 @@ #include "BackupOperationsExecutor.h" #include "DatabaseManager.h" #include "GlobalDBSingleton.h" #include "Logger.h" #include "WorkerThread.h" namespace comm { void BackupOperationsExecutor::createMainCompaction(std::string backupID) { taskType job = [backupID]() { try { DatabaseManager::getQueryExecutor().createMainCompaction(backupID); } catch (const std::exception &e) { // TODO: Inform Rust networking about main // compaction creation failure Logger::log( "Main compaction creation failed. Details: " + std::string(e.what())); } }; GlobalDBSingleton::instance.scheduleOrRunCancellable(job); } void BackupOperationsExecutor::restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey) { taskType job = [mainCompactionPath, mainCompactionEncryptionKey]() { try { DatabaseManager::getQueryExecutor().restoreFromMainCompaction( mainCompactionPath, mainCompactionEncryptionKey); } catch (const std::exception &e) { // TODO: Inform Rust networking about failure // of restoration from main compaction. Logger::log( "Restore from main compaction failed. Details: " + std::string(e.what())); } }; GlobalDBSingleton::instance.scheduleOrRunCancellable(job); } + +void BackupOperationsExecutor::restoreFromBackupLog( + const std::vector &backupLog) { + taskType job = [backupLog]() { + try { + DatabaseManager::getQueryExecutor().restoreFromBackupLog(backupLog); + } catch (const std::exception &e) { + // TODO: Inform Rust networking about failure + // of restoration from backup log. + Logger::log( + "Restore from backup log failed. Details: " + std::string(e.what())); + } + }; + GlobalDBSingleton::instance.scheduleOrRunCancellable(job); +} } // namespace comm diff --git a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.h b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.h index 4d3bbb3f2..7acb24d0f 100644 --- a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.h +++ b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.h @@ -1,13 +1,15 @@ #pragma once #include +#include namespace comm { class BackupOperationsExecutor { public: static void createMainCompaction(std::string backupID); static void restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey); + static void restoreFromBackupLog(const std::vector &backupLog); }; } // namespace comm diff --git a/native/native_rust_library/RustBackupExecutor.cpp b/native/native_rust_library/RustBackupExecutor.cpp index 527b3fa22..9721e8a35 100644 --- a/native/native_rust_library/RustBackupExecutor.cpp +++ b/native/native_rust_library/RustBackupExecutor.cpp @@ -1,40 +1,45 @@ #include "RustBackupExecutor.h" #include "../cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities/BackupOperationsExecutor.h" #include "../cpp/CommonCpp/Tools/PlatformSpecificTools.h" #include namespace comm { rust::String getBackupDirectoryPath() { return rust::String(PlatformSpecificTools::getBackupDirectoryPath()); } rust::String getBackupFilePath(rust::Str backupID, bool isAttachments) { return rust::String(PlatformSpecificTools::getBackupFilePath( std::string(backupID), isAttachments)); } rust::String getBackupLogFilePath(rust::Str backupID, rust::Str logID, bool isAttachments) { return rust::String(PlatformSpecificTools::getBackupLogFilePath( std::string(backupID), std::string(logID), isAttachments)); } rust::String getBackupUserKeysFilePath(rust::Str backupID) { return rust::String( PlatformSpecificTools::getBackupUserKeysFilePath(std::string(backupID))); } void createMainCompaction(rust::String backupID) { BackupOperationsExecutor::createMainCompaction(std::string(backupID)); } void restoreFromMainCompaction( rust::String mainCompactionPath, rust::String mainCompactionEncryptionKey) { BackupOperationsExecutor::restoreFromMainCompaction( std::string(mainCompactionPath), std::string(mainCompactionEncryptionKey)); } + +void restoreFromBackupLog(rust::Vec backupLog) { + BackupOperationsExecutor::restoreFromBackupLog( + std::move(std::vector(backupLog.begin(), backupLog.end()))); +} } // namespace comm diff --git a/native/native_rust_library/RustBackupExecutor.h b/native/native_rust_library/RustBackupExecutor.h index 57d6916d8..0e340a3bf 100644 --- a/native/native_rust_library/RustBackupExecutor.h +++ b/native/native_rust_library/RustBackupExecutor.h @@ -1,17 +1,18 @@ #pragma once #include "cxx.h" namespace comm { rust::String getBackupDirectoryPath(); rust::String getBackupFilePath(rust::Str backupID, bool isAttachments); rust::String getBackupLogFilePath(rust::Str backupID, rust::Str logID, bool isAttachments); rust::String getBackupUserKeysFilePath(rust::Str backupID); void createMainCompaction(rust::String backupID); void restoreFromMainCompaction( rust::String mainCompactionPath, rust::String mainCompactionEncryptionKey); +void restoreFromBackupLog(rust::Vec backupLog); } // namespace comm diff --git a/native/native_rust_library/src/lib.rs b/native/native_rust_library/src/lib.rs index 7a0dc7492..ad0aeb939 100644 --- a/native/native_rust_library/src/lib.rs +++ b/native/native_rust_library/src/lib.rs @@ -1,1266 +1,1270 @@ use backup::ffi::*; use comm_opaque2::client::{Login, Registration}; use comm_opaque2::grpc::opaque_error_to_grpc_status as handle_error; use ffi::{bool_callback, string_callback, void_callback}; use grpc_clients::identity::protos::authenticated::{ InboundKeyInfo, InboundKeysForUserRequest, KeyserverKeysResponse, OutboundKeyInfo, OutboundKeysForUserRequest, RefreshUserPrekeysRequest, UpdateUserPasswordFinishRequest, UpdateUserPasswordStartRequest, UploadOneTimeKeysRequest, }; use grpc_clients::identity::protos::unauth::{ DeviceKeyUpload, DeviceType, Empty, IdentityKeyInfo, OpaqueLoginFinishRequest, OpaqueLoginStartRequest, Prekey, RegistrationFinishRequest, RegistrationStartRequest, WalletLoginRequest, }; use grpc_clients::identity::{ get_auth_client, get_unauthenticated_client, REQUEST_METADATA_COOKIE_KEY, RESPONSE_METADATA_COOKIE_KEY, }; use lazy_static::lazy_static; use serde::Serialize; use std::sync::Arc; use tokio::runtime::{Builder, Runtime}; use tonic::{Request, Status}; use tracing::instrument; mod argon2_tools; mod backup; mod constants; use argon2_tools::compute_backup_key_str; mod generated { // We get the CODE_VERSION from this generated file include!(concat!(env!("OUT_DIR"), "/version.rs")); // We get the IDENTITY_SOCKET_ADDR from this generated file include!(concat!(env!("OUT_DIR"), "/socket_config.rs")); } pub use generated::CODE_VERSION; pub use generated::{BACKUP_SOCKET_ADDR, IDENTITY_SOCKET_ADDR}; #[cfg(not(target_os = "android"))] pub const DEVICE_TYPE: DeviceType = DeviceType::Ios; #[cfg(target_os = "android")] pub const DEVICE_TYPE: DeviceType = DeviceType::Android; lazy_static! { static ref RUNTIME: Arc = Arc::new(Builder::new_multi_thread().enable_all().build().unwrap()); } #[cxx::bridge] mod ffi { extern "Rust" { #[cxx_name = "identityRegisterUser"] fn register_user( username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ); #[cxx_name = "identityLogInPasswordUser"] fn log_in_password_user( username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ); #[cxx_name = "identityLogInWalletUser"] fn log_in_wallet_user( siwe_message: String, siwe_signature: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ); #[cxx_name = "identityUpdateUserPassword"] fn update_user_password( user_id: String, device_id: String, access_token: String, password: String, promise_id: u32, ); #[cxx_name = "identityDeleteUser"] fn delete_user( user_id: String, device_id: String, access_token: String, promise_id: u32, ); #[cxx_name = "identityGetOutboundKeysForUser"] fn get_outbound_keys_for_user( auth_user_id: String, auth_device_id: String, auth_access_token: String, user_id: String, promise_id: u32, ); #[cxx_name = "identityGetInboundKeysForUser"] fn get_inbound_keys_for_user( auth_user_id: String, auth_device_id: String, auth_access_token: String, user_id: String, promise_id: u32, ); #[cxx_name = "identityRefreshUserPrekeys"] fn refresh_user_prekeys( auth_user_id: String, auth_device_id: String, auth_access_token: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, promise_id: u32, ); #[cxx_name = "identityGenerateNonce"] fn generate_nonce(promise_id: u32); #[cxx_name = "identityVersionSupported"] fn version_supported(promise_id: u32); #[cxx_name = "identityUploadOneTimeKeys"] fn upload_one_time_keys( auth_user_id: String, auth_device_id: String, auth_access_token: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ); #[cxx_name = "identityGetKeyserverKeys"] fn get_keyserver_keys( user_id: String, device_id: String, access_token: String, keyserver_id: String, promise_id: u32, ); // Argon2 #[cxx_name = "compute_backup_key"] fn compute_backup_key_str( password: &str, backup_id: &str, ) -> Result<[u8; 32]>; } unsafe extern "C++" { include!("RustCallback.h"); #[namespace = "comm"] #[cxx_name = "stringCallback"] fn string_callback(error: String, promise_id: u32, ret: String); #[namespace = "comm"] #[cxx_name = "voidCallback"] fn void_callback(error: String, promise_id: u32); #[namespace = "comm"] #[cxx_name = "boolCallback"] fn bool_callback(error: String, promise_id: u32, ret: bool); } // AES cryptography #[namespace = "comm"] unsafe extern "C++" { include!("RustAESCrypto.h"); #[allow(unused)] #[cxx_name = "aesGenerateKey"] fn generate_key(buffer: &mut [u8]) -> Result<()>; /// The first two argument aren't mutated but creation of Java ByteBuffer /// requires the underlying bytes to be mutable. #[allow(unused)] #[cxx_name = "aesEncrypt"] fn encrypt( key: &mut [u8], plaintext: &mut [u8], sealed_data: &mut [u8], ) -> Result<()>; /// The first two argument aren't mutated but creation of Java ByteBuffer /// requires the underlying bytes to be mutable. #[allow(unused)] #[cxx_name = "aesDecrypt"] fn decrypt( key: &mut [u8], sealed_data: &mut [u8], plaintext: &mut [u8], ) -> Result<()>; } // Comm Services Auth Metadata Emission #[namespace = "comm"] unsafe extern "C++" { include!("RustCSAMetadataEmitter.h"); #[allow(unused)] #[cxx_name = "sendAuthMetadataToJS"] fn send_auth_metadata_to_js( access_token: String, user_id: String, ) -> Result<()>; } // Backup extern "Rust" { #[cxx_name = "startBackupHandler"] fn start_backup_handler() -> Result<()>; #[cxx_name = "stopBackupHandler"] fn stop_backup_handler() -> Result<()>; #[cxx_name = "triggerBackupFileUpload"] fn trigger_backup_file_upload(); #[cxx_name = "createBackup"] fn create_backup_sync( backup_id: String, backup_secret: String, pickle_key: String, pickled_account: String, user_data: String, promise_id: u32, ); #[cxx_name = "restoreBackup"] fn restore_backup_sync(backup_secret: String, promise_id: u32); } // Secure store #[namespace = "comm"] unsafe extern "C++" { include!("RustSecureStore.h"); #[allow(unused)] #[cxx_name = "secureStoreSet"] fn secure_store_set(key: &str, value: String) -> Result<()>; #[cxx_name = "secureStoreGet"] fn secure_store_get(key: &str) -> Result; } // C++ Backup creation #[namespace = "comm"] unsafe extern "C++" { include!("RustBackupExecutor.h"); #[allow(unused)] #[cxx_name = "getBackupDirectoryPath"] fn get_backup_directory_path() -> Result; #[allow(unused)] #[cxx_name = "getBackupFilePath"] fn get_backup_file_path( backup_id: &str, is_attachments: bool, ) -> Result; #[allow(unused)] #[cxx_name = "getBackupLogFilePath"] fn get_backup_log_file_path( backup_id: &str, log_id: &str, is_attachments: bool, ) -> Result; #[allow(unused)] #[cxx_name = "getBackupUserKeysFilePath"] fn get_backup_user_keys_file_path(backup_id: &str) -> Result; #[cxx_name = "createMainCompaction"] fn create_main_compaction(backup_id: String) -> Result<()>; #[allow(unused)] #[cxx_name = "restoreFromMainCompaction"] fn restore_from_main_compaction( main_compaction_path: String, main_compaction_encryption_key: String, ) -> Result<()>; + + #[allow(unused)] + #[cxx_name = "restoreFromBackupLog"] + fn restore_from_backup_log(backup_log: Vec) -> Result<()>; } } fn handle_string_result_as_callback( result: Result, promise_id: u32, ) where E: std::fmt::Display, { match result { Err(e) => string_callback(e.to_string(), promise_id, "".to_string()), Ok(r) => string_callback("".to_string(), promise_id, r), } } fn handle_void_result_as_callback(result: Result<(), E>, promise_id: u32) where E: std::fmt::Display, { match result { Err(e) => void_callback(e.to_string(), promise_id), Ok(_) => void_callback("".to_string(), promise_id), } } fn handle_bool_result_as_callback(result: Result, promise_id: u32) where E: std::fmt::Display, { match result { Err(e) => bool_callback(e.to_string(), promise_id, false), Ok(r) => bool_callback("".to_string(), promise_id, r), } } fn generate_nonce(promise_id: u32) { RUNTIME.spawn(async move { let result = fetch_nonce().await; handle_string_result_as_callback(result, promise_id); }); } async fn fetch_nonce() -> Result { let mut identity_client = get_unauthenticated_client( IDENTITY_SOCKET_ADDR, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let nonce = identity_client .generate_nonce(Empty {}) .await? .into_inner() .nonce; Ok(nonce) } fn version_supported(promise_id: u32) { RUNTIME.spawn(async move { let result = version_supported_helper().await; handle_bool_result_as_callback(result, promise_id); }); } async fn version_supported_helper() -> Result { let mut identity_client = get_unauthenticated_client( IDENTITY_SOCKET_ADDR, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let response = identity_client.ping(Empty {}).await; match response { Ok(_) => Ok(true), Err(e) => { if grpc_clients::error::is_version_unsupported(&e) { Ok(false) } else { Err(e.into()) } } } } fn get_keyserver_keys( user_id: String, device_id: String, access_token: String, keyserver_id: String, promise_id: u32, ) { RUNTIME.spawn(async move { let get_keyserver_keys_request = OutboundKeysForUserRequest { user_id: keyserver_id, }; let auth_info = AuthInfo { access_token, user_id, device_id, }; let result = get_keyserver_keys_helper(get_keyserver_keys_request, auth_info).await; handle_string_result_as_callback(result, promise_id); }); } async fn get_keyserver_keys_helper( get_keyserver_keys_request: OutboundKeysForUserRequest, auth_info: AuthInfo, ) -> Result { let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let response = identity_client .get_keyserver_keys(get_keyserver_keys_request) .await? .into_inner(); let keyserver_keys = OutboundKeyInfoResponse::try_from(response)?; Ok(serde_json::to_string(&keyserver_keys)?) } struct AuthInfo { user_id: String, device_id: String, access_token: String, } #[instrument] fn register_user( username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ) { RUNTIME.spawn(async move { let password_user_info = PasswordUserInfo { username, password, key_payload, key_payload_signature, content_prekey, content_prekey_signature, notif_prekey, notif_prekey_signature, content_one_time_keys, notif_one_time_keys, }; let result = register_user_helper(password_user_info).await; handle_string_result_as_callback(result, promise_id); }); } struct PasswordUserInfo { username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct UserIDAndDeviceAccessToken { #[serde(rename = "userID")] user_id: String, access_token: String, } async fn register_user_helper( password_user_info: PasswordUserInfo, ) -> Result { let mut client_registration = Registration::new(); let opaque_registration_request = client_registration .start(&password_user_info.password) .map_err(handle_error)?; let registration_start_request = RegistrationStartRequest { opaque_registration_request, username: password_user_info.username, device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: password_user_info.key_payload, payload_signature: password_user_info.key_payload_signature, social_proof: None, }), content_upload: Some(Prekey { prekey: password_user_info.content_prekey, prekey_signature: password_user_info.content_prekey_signature, }), notif_upload: Some(Prekey { prekey: password_user_info.notif_prekey, prekey_signature: password_user_info.notif_prekey_signature, }), one_time_content_prekeys: password_user_info.content_one_time_keys, one_time_notif_prekeys: password_user_info.notif_one_time_keys, device_type: DEVICE_TYPE.into(), }), }; let mut identity_client = get_unauthenticated_client( IDENTITY_SOCKET_ADDR, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let response = identity_client .register_password_user_start(registration_start_request) .await?; // We need to get the load balancer cookie from from the response and send it // in the subsequent request to ensure it is routed to the same identity // service instance as the first request let cookie = response .metadata() .get(RESPONSE_METADATA_COOKIE_KEY) .cloned(); let registration_start_response = response.into_inner(); let opaque_registration_upload = client_registration .finish( &password_user_info.password, ®istration_start_response.opaque_registration_response, ) .map_err(handle_error)?; let registration_finish_request = RegistrationFinishRequest { session_id: registration_start_response.session_id, opaque_registration_upload, }; let mut finish_request = Request::new(registration_finish_request); // Cookie won't be available in local dev environments if let Some(cookie_metadata) = cookie { finish_request .metadata_mut() .insert(REQUEST_METADATA_COOKIE_KEY, cookie_metadata); } let registration_finish_response = identity_client .register_password_user_finish(finish_request) .await? .into_inner(); let user_id_and_access_token = UserIDAndDeviceAccessToken { user_id: registration_finish_response.user_id, access_token: registration_finish_response.access_token, }; Ok(serde_json::to_string(&user_id_and_access_token)?) } #[instrument] fn log_in_password_user( username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ) { RUNTIME.spawn(async move { let password_user_info = PasswordUserInfo { username, password, key_payload, key_payload_signature, content_prekey, content_prekey_signature, notif_prekey, notif_prekey_signature, content_one_time_keys, notif_one_time_keys, }; let result = log_in_password_user_helper(password_user_info).await; handle_string_result_as_callback(result, promise_id); }); } async fn log_in_password_user_helper( password_user_info: PasswordUserInfo, ) -> Result { let mut client_login = Login::new(); let opaque_login_request = client_login .start(&password_user_info.password) .map_err(handle_error)?; let login_start_request = OpaqueLoginStartRequest { opaque_login_request, username: password_user_info.username, device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: password_user_info.key_payload, payload_signature: password_user_info.key_payload_signature, social_proof: None, }), content_upload: Some(Prekey { prekey: password_user_info.content_prekey, prekey_signature: password_user_info.content_prekey_signature, }), notif_upload: Some(Prekey { prekey: password_user_info.notif_prekey, prekey_signature: password_user_info.notif_prekey_signature, }), one_time_content_prekeys: password_user_info.content_one_time_keys, one_time_notif_prekeys: password_user_info.notif_one_time_keys, device_type: DEVICE_TYPE.into(), }), }; let mut identity_client = get_unauthenticated_client( IDENTITY_SOCKET_ADDR, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let response = identity_client .log_in_password_user_start(login_start_request) .await?; // We need to get the load balancer cookie from from the response and send it // in the subsequent request to ensure it is routed to the same identity // service instance as the first request let cookie = response .metadata() .get(RESPONSE_METADATA_COOKIE_KEY) .cloned(); let login_start_response = response.into_inner(); let opaque_login_upload = client_login .finish(&login_start_response.opaque_login_response) .map_err(handle_error)?; let login_finish_request = OpaqueLoginFinishRequest { session_id: login_start_response.session_id, opaque_login_upload, }; let mut finish_request = Request::new(login_finish_request); // Cookie won't be available in local dev environments if let Some(cookie_metadata) = cookie { finish_request .metadata_mut() .insert(REQUEST_METADATA_COOKIE_KEY, cookie_metadata); } let login_finish_response = identity_client .log_in_password_user_finish(finish_request) .await? .into_inner(); let user_id_and_access_token = UserIDAndDeviceAccessToken { user_id: login_finish_response.user_id, access_token: login_finish_response.access_token, }; Ok(serde_json::to_string(&user_id_and_access_token)?) } struct WalletUserInfo { siwe_message: String, siwe_signature: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, } #[instrument] fn log_in_wallet_user( siwe_message: String, siwe_signature: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ) { RUNTIME.spawn(async move { let wallet_user_info = WalletUserInfo { siwe_message, siwe_signature, key_payload, key_payload_signature, content_prekey, content_prekey_signature, notif_prekey, notif_prekey_signature, content_one_time_keys, notif_one_time_keys, }; let result = log_in_wallet_user_helper(wallet_user_info).await; handle_string_result_as_callback(result, promise_id); }); } async fn log_in_wallet_user_helper( wallet_user_info: WalletUserInfo, ) -> Result { let login_request = WalletLoginRequest { siwe_message: wallet_user_info.siwe_message, siwe_signature: wallet_user_info.siwe_signature, device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: wallet_user_info.key_payload, payload_signature: wallet_user_info.key_payload_signature, social_proof: None, // The SIWE message and signature are the social proof }), content_upload: Some(Prekey { prekey: wallet_user_info.content_prekey, prekey_signature: wallet_user_info.content_prekey_signature, }), notif_upload: Some(Prekey { prekey: wallet_user_info.notif_prekey, prekey_signature: wallet_user_info.notif_prekey_signature, }), one_time_content_prekeys: wallet_user_info.content_one_time_keys, one_time_notif_prekeys: wallet_user_info.notif_one_time_keys, device_type: DEVICE_TYPE.into(), }), }; let mut identity_client = get_unauthenticated_client( IDENTITY_SOCKET_ADDR, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let login_response = identity_client .log_in_wallet_user(login_request) .await? .into_inner(); let user_id_and_access_token = UserIDAndDeviceAccessToken { user_id: login_response.user_id, access_token: login_response.access_token, }; Ok(serde_json::to_string(&user_id_and_access_token)?) } struct UpdatePasswordInfo { user_id: String, device_id: String, access_token: String, password: String, } fn update_user_password( user_id: String, device_id: String, access_token: String, password: String, promise_id: u32, ) { RUNTIME.spawn(async move { let update_password_info = UpdatePasswordInfo { access_token, user_id, device_id, password, }; let result = update_user_password_helper(update_password_info).await; handle_void_result_as_callback(result, promise_id); }); } async fn update_user_password_helper( update_password_info: UpdatePasswordInfo, ) -> Result<(), Error> { let mut client_registration = Registration::new(); let opaque_registration_request = client_registration .start(&update_password_info.password) .map_err(handle_error)?; let update_password_start_request = UpdateUserPasswordStartRequest { opaque_registration_request, }; let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, update_password_info.user_id, update_password_info.device_id, update_password_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let response = identity_client .update_user_password_start(update_password_start_request) .await?; // We need to get the load balancer cookie from from the response and send it // in the subsequent request to ensure it is routed to the same identity // service instance as the first request let cookie = response .metadata() .get(RESPONSE_METADATA_COOKIE_KEY) .cloned(); let update_password_start_response = response.into_inner(); let opaque_registration_upload = client_registration .finish( &update_password_info.password, &update_password_start_response.opaque_registration_response, ) .map_err(handle_error)?; let update_password_finish_request = UpdateUserPasswordFinishRequest { session_id: update_password_start_response.session_id, opaque_registration_upload, }; let mut finish_request = Request::new(update_password_finish_request); // Cookie won't be available in local dev environments if let Some(cookie_metadata) = cookie { finish_request .metadata_mut() .insert(REQUEST_METADATA_COOKIE_KEY, cookie_metadata); } identity_client .update_user_password_finish(finish_request) .await?; Ok(()) } fn delete_user( user_id: String, device_id: String, access_token: String, promise_id: u32, ) { RUNTIME.spawn(async move { let auth_info = AuthInfo { access_token, user_id, device_id, }; let result = delete_user_helper(auth_info).await; handle_void_result_as_callback(result, promise_id); }); } async fn delete_user_helper(auth_info: AuthInfo) -> Result<(), Error> { let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; identity_client.delete_user(Empty {}).await?; Ok(()) } struct GetOutboundKeysRequestInfo { user_id: String, } struct GetInboundKeysRequestInfo { user_id: String, } // This struct should not be altered without also updating // OutboundKeyInfoResponse in lib/types/identity-service-types.js #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct OutboundKeyInfoResponse { pub payload: String, pub payload_signature: String, pub social_proof: Option, pub content_prekey: String, pub content_prekey_signature: String, pub notif_prekey: String, pub notif_prekey_signature: String, pub one_time_content_prekey: Option, pub one_time_notif_prekey: Option, } // This struct should not be altered without also updating // InboundKeyInfoResponse in lib/types/identity-service-types.js #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct InboundKeyInfoResponse { pub payload: String, pub payload_signature: String, pub social_proof: Option, pub content_prekey: String, pub content_prekey_signature: String, pub notif_prekey: String, pub notif_prekey_signature: String, } impl TryFrom for OutboundKeyInfoResponse { type Error = Error; fn try_from(key_info: OutboundKeyInfo) -> Result { let identity_info = key_info.identity_info.ok_or(Error::MissingResponseData)?; let IdentityKeyInfo { payload, payload_signature, social_proof, } = identity_info; let content_prekey = key_info.content_prekey.ok_or(Error::MissingResponseData)?; let Prekey { prekey: content_prekey_value, prekey_signature: content_prekey_signature, } = content_prekey; let notif_prekey = key_info.notif_prekey.ok_or(Error::MissingResponseData)?; let Prekey { prekey: notif_prekey_value, prekey_signature: notif_prekey_signature, } = notif_prekey; let one_time_content_prekey = key_info.one_time_content_prekey; let one_time_notif_prekey = key_info.one_time_notif_prekey; Ok(Self { payload, payload_signature, social_proof, content_prekey: content_prekey_value, content_prekey_signature, notif_prekey: notif_prekey_value, notif_prekey_signature, one_time_content_prekey, one_time_notif_prekey, }) } } impl TryFrom for OutboundKeyInfoResponse { type Error = Error; fn try_from(response: KeyserverKeysResponse) -> Result { let key_info = response.keyserver_info.ok_or(Error::MissingResponseData)?; Self::try_from(key_info) } } fn get_outbound_keys_for_user( auth_user_id: String, auth_device_id: String, auth_access_token: String, user_id: String, promise_id: u32, ) { RUNTIME.spawn(async move { let get_outbound_keys_request_info = GetOutboundKeysRequestInfo { user_id }; let auth_info = AuthInfo { access_token: auth_access_token, user_id: auth_user_id, device_id: auth_device_id, }; let result = get_outbound_keys_for_user_helper( get_outbound_keys_request_info, auth_info, ) .await; handle_string_result_as_callback(result, promise_id); }); } async fn get_outbound_keys_for_user_helper( get_outbound_keys_request_info: GetOutboundKeysRequestInfo, auth_info: AuthInfo, ) -> Result { let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let response = identity_client .get_outbound_keys_for_user(OutboundKeysForUserRequest { user_id: get_outbound_keys_request_info.user_id, }) .await? .into_inner(); let outbound_key_info: Vec = response .devices .into_values() .map(OutboundKeyInfoResponse::try_from) .collect::, _>>()?; Ok(serde_json::to_string(&outbound_key_info)?) } impl TryFrom for InboundKeyInfoResponse { type Error = Error; fn try_from(key_info: InboundKeyInfo) -> Result { let identity_info = key_info.identity_info.ok_or(Error::MissingResponseData)?; let IdentityKeyInfo { payload, payload_signature, social_proof, } = identity_info; let content_prekey = key_info.content_prekey.ok_or(Error::MissingResponseData)?; let Prekey { prekey: content_prekey_value, prekey_signature: content_prekey_signature, } = content_prekey; let notif_prekey = key_info.notif_prekey.ok_or(Error::MissingResponseData)?; let Prekey { prekey: notif_prekey_value, prekey_signature: notif_prekey_signature, } = notif_prekey; Ok(Self { payload, payload_signature, social_proof, content_prekey: content_prekey_value, content_prekey_signature, notif_prekey: notif_prekey_value, notif_prekey_signature, }) } } fn get_inbound_keys_for_user( auth_user_id: String, auth_device_id: String, auth_access_token: String, user_id: String, promise_id: u32, ) { RUNTIME.spawn(async move { let get_inbound_keys_request_info = GetInboundKeysRequestInfo { user_id }; let auth_info = AuthInfo { access_token: auth_access_token, user_id: auth_user_id, device_id: auth_device_id, }; let result = get_inbound_keys_for_user_helper( get_inbound_keys_request_info, auth_info, ) .await; handle_string_result_as_callback(result, promise_id); }); } async fn get_inbound_keys_for_user_helper( get_inbound_keys_request_info: GetInboundKeysRequestInfo, auth_info: AuthInfo, ) -> Result { let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let response = identity_client .get_inbound_keys_for_user(InboundKeysForUserRequest { user_id: get_inbound_keys_request_info.user_id, }) .await? .into_inner(); let inbound_key_info: Vec = response .devices .into_values() .map(InboundKeyInfoResponse::try_from) .collect::, _>>()?; Ok(serde_json::to_string(&inbound_key_info)?) } fn refresh_user_prekeys( auth_user_id: String, auth_device_id: String, auth_access_token: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, promise_id: u32, ) { RUNTIME.spawn(async move { let refresh_request = RefreshUserPrekeysRequest { new_content_prekeys: Some(Prekey { prekey: content_prekey, prekey_signature: content_prekey_signature, }), new_notif_prekeys: Some(Prekey { prekey: notif_prekey, prekey_signature: notif_prekey_signature, }), }; let auth_info = AuthInfo { access_token: auth_access_token, user_id: auth_user_id, device_id: auth_device_id, }; let result = refresh_user_prekeys_helper(refresh_request, auth_info).await; handle_void_result_as_callback(result, promise_id); }); } async fn refresh_user_prekeys_helper( refresh_request: RefreshUserPrekeysRequest, auth_info: AuthInfo, ) -> Result<(), Error> { get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await? .refresh_user_prekeys(refresh_request) .await?; Ok(()) } #[instrument] fn upload_one_time_keys( auth_user_id: String, auth_device_id: String, auth_access_token: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ) { RUNTIME.spawn(async move { let upload_request = UploadOneTimeKeysRequest { content_one_time_prekeys: content_one_time_keys, notif_one_time_prekeys: notif_one_time_keys, }; let auth_info = AuthInfo { access_token: auth_access_token, user_id: auth_user_id, device_id: auth_device_id, }; let result = upload_one_time_keys_helper(auth_info, upload_request).await; handle_void_result_as_callback(result, promise_id); }); } async fn upload_one_time_keys_helper( auth_info: AuthInfo, upload_request: UploadOneTimeKeysRequest, ) -> Result<(), Error> { let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; identity_client.upload_one_time_keys(upload_request).await?; Ok(()) } #[derive( Debug, derive_more::Display, derive_more::From, derive_more::Error, )] pub enum Error { #[display(fmt = "{}", "_0.message()")] TonicGRPC(Status), #[display(fmt = "{}", "_0")] SerdeJson(serde_json::Error), #[display(fmt = "Missing response data")] MissingResponseData, #[display(fmt = "{}", "_0")] GRPClient(grpc_clients::error::Error), } #[cfg(test)] mod tests { use super::{BACKUP_SOCKET_ADDR, CODE_VERSION, IDENTITY_SOCKET_ADDR}; #[test] fn test_code_version_exists() { assert!(CODE_VERSION > 0); } #[test] fn test_identity_socket_addr_exists() { assert!(IDENTITY_SOCKET_ADDR.len() > 0); assert!(BACKUP_SOCKET_ADDR.len() > 0); } } diff --git a/web/cpp/SQLiteQueryExecutorBindings.cpp b/web/cpp/SQLiteQueryExecutorBindings.cpp index e9266d105..9150b6af8 100644 --- a/web/cpp/SQLiteQueryExecutorBindings.cpp +++ b/web/cpp/SQLiteQueryExecutorBindings.cpp @@ -1,216 +1,218 @@ #include "SQLiteQueryExecutor.cpp" #include "entities/Nullable.h" #include #include namespace comm { using namespace emscripten; std::string getExceptionMessage(int exceptionPtr) { if (exceptionPtr == 0) { return std::string("Exception pointer value was null"); } std::exception *e = reinterpret_cast(exceptionPtr); if (e) { return std::string(e->what()); } return std::string("Pointer to exception was invalid"); } EMSCRIPTEN_BINDINGS(SQLiteQueryExecutor) { function("getExceptionMessage", &getExceptionMessage); value_object("NullableString") .field("value", &NullableString::value) .field("isNull", &NullableString::isNull); value_object("NullableInt") .field("value", &NullableInt::value) .field("isNull", &NullableInt::isNull); value_object("Draft") .field("key", &Draft::key) .field("text", &Draft::text); value_object("Report") .field("id", &Report::id) .field("report", &Report::report); value_object("PersistItem") .field("key", &PersistItem::key) .field("item", &PersistItem::item); value_object("UserInfo") .field("id", &UserInfo::id) .field("userInfo", &UserInfo::user_info); value_object("KeyserverInfo") .field("id", &KeyserverInfo::id) .field("keyserverInfo", &KeyserverInfo::keyserver_info); value_object("MessageStoreThreads") .field("id", &MessageStoreThread::id) .field("startReached", &MessageStoreThread::start_reached); value_object("WebThread") .field("id", &WebThread::id) .field("type", &WebThread::type) .field("name", &WebThread::name) .field("description", &WebThread::description) .field("color", &WebThread::color) .field("creationTime", &WebThread::creation_time) .field("parentThreadID", &WebThread::parent_thread_id) .field("containingThreadID", &WebThread::containing_thread_id) .field("community", &WebThread::community) .field("members", &WebThread::members) .field("roles", &WebThread::roles) .field("currentUser", &WebThread::current_user) .field("sourceMessageID", &WebThread::source_message_id) .field("repliesCount", &WebThread::replies_count) .field("avatar", &WebThread::avatar) .field("pinnedCount", &WebThread::pinned_count); value_object("WebMessage") .field("id", &WebMessage::id) .field("localID", &WebMessage::local_id) .field("thread", &WebMessage::thread) .field("user", &WebMessage::user) .field("type", &WebMessage::type) .field("futureType", &WebMessage::future_type) .field("content", &WebMessage::content) .field("time", &WebMessage::time); value_object("Media") .field("id", &Media::id) .field("container", &Media::container) .field("thread", &Media::thread) .field("uri", &Media::uri) .field("type", &Media::type) .field("extras", &Media::extras); value_object("MessageWithMedias") .field("message", &MessageWithMedias::message) .field("medias", &MessageWithMedias::medias); value_object("OlmPersistSession") .field("targetUserID", &OlmPersistSession::target_user_id) .field("sessionData", &OlmPersistSession::session_data); class_("SQLiteQueryExecutor") .constructor() .function("updateDraft", &SQLiteQueryExecutor::updateDraft) .function("moveDraft", &SQLiteQueryExecutor::moveDraft) .function("getAllDrafts", &SQLiteQueryExecutor::getAllDrafts) .function("removeAllDrafts", &SQLiteQueryExecutor::removeAllDrafts) .function("removeDrafts", &SQLiteQueryExecutor::removeDrafts) .function("getAllMessagesWeb", &SQLiteQueryExecutor::getAllMessagesWeb) .function("removeAllMessages", &SQLiteQueryExecutor::removeAllMessages) .function("removeMessages", &SQLiteQueryExecutor::removeMessages) .function( "removeMessagesForThreads", &SQLiteQueryExecutor::removeMessagesForThreads) .function("replaceMessageWeb", &SQLiteQueryExecutor::replaceMessageWeb) .function("rekeyMessage", &SQLiteQueryExecutor::rekeyMessage) .function("removeAllMedia", &SQLiteQueryExecutor::removeAllMedia) .function( "removeMediaForThreads", &SQLiteQueryExecutor::removeMediaForThreads) .function( "removeMediaForMessage", &SQLiteQueryExecutor::removeMediaForMessage) .function( "removeMediaForMessages", &SQLiteQueryExecutor::removeMediaForMessages) .function("replaceMedia", &SQLiteQueryExecutor::replaceMedia) .function( "rekeyMediaContainers", &SQLiteQueryExecutor::rekeyMediaContainers) .function( "replaceMessageStoreThreads", &SQLiteQueryExecutor::replaceMessageStoreThreads) .function( "removeMessageStoreThreads", &SQLiteQueryExecutor::removeMessageStoreThreads) .function( "getAllMessageStoreThreads", &SQLiteQueryExecutor::getAllMessageStoreThreads) .function( "removeAllMessageStoreThreads", &SQLiteQueryExecutor::removeAllMessageStoreThreads) .function("setMetadata", &SQLiteQueryExecutor::setMetadata) .function("clearMetadata", &SQLiteQueryExecutor::clearMetadata) .function("getMetadata", &SQLiteQueryExecutor::getMetadata) .function("replaceReport", &SQLiteQueryExecutor::replaceReport) .function("removeReports", &SQLiteQueryExecutor::removeReports) .function("removeAllReports", &SQLiteQueryExecutor::removeAllReports) .function("getAllReports", &SQLiteQueryExecutor::getAllReports) .function( "setPersistStorageItem", &SQLiteQueryExecutor::setPersistStorageItem) .function( "removePersistStorageItem", &SQLiteQueryExecutor::removePersistStorageItem) .function( "getPersistStorageItem", &SQLiteQueryExecutor::getPersistStorageItem) .function("replaceUser", &SQLiteQueryExecutor::replaceUser) .function("removeUsers", &SQLiteQueryExecutor::removeUsers) .function("removeAllUsers", &SQLiteQueryExecutor::removeAllUsers) .function("getAllUsers", &SQLiteQueryExecutor::getAllUsers) .function("replaceThreadWeb", &SQLiteQueryExecutor::replaceThreadWeb) .function("getAllThreadsWeb", &SQLiteQueryExecutor::getAllThreadsWeb) .function("removeAllThreads", &SQLiteQueryExecutor::removeAllThreads) .function("removeThreads", &SQLiteQueryExecutor::removeThreads) .function("replaceKeyserver", &SQLiteQueryExecutor::replaceKeyserver) .function("removeKeyservers", &SQLiteQueryExecutor::removeKeyservers) .function( "removeAllKeyservers", &SQLiteQueryExecutor::removeAllKeyservers) .function("getAllKeyservers", &SQLiteQueryExecutor::getAllKeyservers) .function("beginTransaction", &SQLiteQueryExecutor::beginTransaction) .function("commitTransaction", &SQLiteQueryExecutor::commitTransaction) .function( "getOlmPersistSessionsData", &SQLiteQueryExecutor::getOlmPersistSessionsData) .function( "getOlmPersistAccountDataWeb", &SQLiteQueryExecutor::getOlmPersistAccountDataWeb) .function( "storeOlmPersistSession", &SQLiteQueryExecutor::storeOlmPersistSession) .function( "storeOlmPersistAccount", &SQLiteQueryExecutor::storeOlmPersistAccount) .function( "rollbackTransaction", &SQLiteQueryExecutor::rollbackTransaction) .function( "restoreFromMainCompaction", - &SQLiteQueryExecutor::restoreFromMainCompaction); + &SQLiteQueryExecutor::restoreFromMainCompaction) + .function( + "restoreFromBackupLog", &SQLiteQueryExecutor::restoreFromBackupLog); } } // namespace comm namespace emscripten { namespace internal { template struct BindingType> { using ValBinding = BindingType; using WireType = ValBinding::WireType; static WireType toWireType(const std::vector &vec) { std::vector valVec(vec.begin(), vec.end()); return BindingType::toWireType(val::array(valVec)); } static std::vector fromWireType(WireType value) { return vecFromJSArray(ValBinding::fromWireType(value)); } }; template struct TypeID< T, typename std::enable_if_t::type, std::vector< typename Canonicalized::type::value_type, typename Canonicalized::type::allocator_type>>::value>> { static constexpr TYPEID get() { return TypeID::get(); } }; } // namespace internal } // namespace emscripten diff --git a/web/database/_generated/comm-query-executor.js b/web/database/_generated/comm-query-executor.js index 99689a41e..0f89ff565 100644 --- a/web/database/_generated/comm-query-executor.js +++ b/web/database/_generated/comm-query-executor.js @@ -1,158 +1,158 @@ // @generated var Module = (() => { var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename; return ( function(Module) { Module = Module || {}; var e;e||(e=typeof Module !== 'undefined' ? Module : {});var aa,ba;e.ready=new Promise(function(a,b){aa=a;ba=b});var ca=Object.assign({},e),da="./this.program",ea="object"==typeof window,fa="function"==typeof importScripts,ha="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,m="",ia,ja,ka,fs,la,ma; if(ha)m=fa?require("path").dirname(m)+"/":__dirname+"/",ma=()=>{la||(fs=require("fs"),la=require("path"))},ia=function(a,b){ma();a=la.normalize(a);return fs.readFileSync(a,b?void 0:"utf8")},ka=a=>{a=ia(a,!0);a.buffer||(a=new Uint8Array(a));return a},ja=(a,b,c)=>{ma();a=la.normalize(a);fs.readFile(a,function(d,f){d?c(d):b(f.buffer)})},1{var b=new XMLHttpRequest;b.open("GET",a,!1);b.send(null);return b.responseText},fa&&(ka=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),ja=(a,b,c)=>{var d=new XMLHttpRequest;d.open("GET",a,!0);d.responseType="arraybuffer"; d.onload=()=>{200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};var na=e.print||console.log.bind(console),r=e.printErr||console.warn.bind(console);Object.assign(e,ca);ca=null;e.thisProgram&&(da=e.thisProgram);var oa;e.wasmBinary&&(oa=e.wasmBinary);var noExitRuntime=e.noExitRuntime||!0;"object"!=typeof WebAssembly&&v("no native wasm support detected");var pa,qa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0; function sa(a,b,c){var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16f?d+=String.fromCharCode(f):(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else d+=String.fromCharCode(f)}return d}function y(a,b){return a?sa(z,a,b):""} function A(a,b,c,d){if(!(0=k){var h=a.charCodeAt(++g);k=65536+((k&1023)<<10)|h&1023}if(127>=k){if(c>=d)break;b[c++]=k}else{if(2047>=k){if(c+1>=d)break;b[c++]=192|k>>6}else{if(65535>=k){if(c+2>=d)break;b[c++]=224|k>>12}else{if(c+3>=d)break;b[c++]=240|k>>18;b[c++]=128|k>>12&63}b[c++]=128|k>>6&63}b[c++]=128|k&63}}b[c]=0;return c-f} function ta(a){for(var b=0,c=0;c=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b}var ua,B,z,D,va,E,F,wa,xa;function ya(){var a=pa.buffer;ua=a;e.HEAP8=B=new Int8Array(a);e.HEAP16=D=new Int16Array(a);e.HEAP32=E=new Int32Array(a);e.HEAPU8=z=new Uint8Array(a);e.HEAPU16=va=new Uint16Array(a);e.HEAPU32=F=new Uint32Array(a);e.HEAPF32=wa=new Float32Array(a);e.HEAPF64=xa=new Float64Array(a)}var za,Aa=[],Ba=[],Ca=[]; function Da(){var a=e.preRun.shift();Aa.unshift(a)}var Ea=0,Fa=null,Ha=null;function Ia(){Ea++;e.monitorRunDependencies&&e.monitorRunDependencies(Ea)}function Ja(){Ea--;e.monitorRunDependencies&&e.monitorRunDependencies(Ea);if(0==Ea&&(null!==Fa&&(clearInterval(Fa),Fa=null),Ha)){var a=Ha;Ha=null;a()}}function v(a){if(e.onAbort)e.onAbort(a);a="Aborted("+a+")";r(a);qa=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");ba(a);throw a;}var Ka;Ka="comm-query-executor.wasm"; if(!Ka.startsWith("data:application/octet-stream;base64,")){var La=Ka;Ka=e.locateFile?e.locateFile(La,m):m+La} function Ma(a){var b=Ka;try{a:{try{if(b==Ka&&oa){var c=new Uint8Array(oa);break a}if(ka){c=ka(b);break a}throw"sync fetching of the wasm failed: you can preload it to Module['wasmBinary'] manually, or emcc.py will do that for you when generating HTML (but not JS)";}catch(g){v(g)}c=void 0}var d=new WebAssembly.Module(c);var f=new WebAssembly.Instance(d,a)}catch(g){throw a=g.toString(),r("failed to compile wasm module: "+a),(a.includes("imported Memory")||a.includes("memory import"))&&r("Memory size incompatibility issues may be due to changing INITIAL_MEMORY at runtime to something too large. Use ALLOW_MEMORY_GROWTH to allow any size memory (and also make sure not to set INITIAL_MEMORY at runtime to something smaller than it was at compile time)."), g;}return[f,d]}var G,J;function Na(a){for(;0>2]=b};this.Xc=function(b){F[this.Sa+8>>2]=b};this.Yc=function(){E[this.Sa>>2]=0};this.ac=function(){B[this.Sa+12>>0]=0};this.Zc=function(){B[this.Sa+13>>0]=0};this.Db=function(b,c){this.cb();this.$c(b);this.Xc(c);this.Yc();this.ac();this.Zc()};this.cb=function(){F[this.Sa+16>>2]=0}} var Pa=0,Qa=(a,b)=>{for(var c=0,d=a.length-1;0<=d;d--){var f=a[d];"."===f?a.splice(d,1):".."===f?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c;c--)a.unshift("..");return a},L=a=>{var b="/"===a.charAt(0),c="/"===a.substr(-1);(a=Qa(a.split("/").filter(d=>!!d),!b).join("/"))||b||(a=".");a&&c&&(a+="/");return(b?"/":"")+a},Ra=a=>{var b=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(a).slice(1);a=b[0];b=b[1];if(!a&&!b)return".";b&&(b=b.substr(0,b.length-1));return a+b},M= a=>{if("/"===a)return"/";a=L(a);a=a.replace(/\/$/,"");var b=a.lastIndexOf("/");return-1===b?a:a.substr(b+1)},Sa=(a,b)=>L(a+"/"+b);function Ta(){if("object"==typeof crypto&&"function"==typeof crypto.getRandomValues){var a=new Uint8Array(1);return()=>{crypto.getRandomValues(a);return a[0]}}if(ha)try{var b=require("crypto");return()=>b.randomBytes(1)[0]}catch(c){}return()=>v("randomDevice")} function Ua(){for(var a="",b=!1,c=arguments.length-1;-1<=c&&!b;c--){b=0<=c?arguments[c]:N.cwd();if("string"!=typeof b)throw new TypeError("Arguments to path.resolve must be strings");if(!b)return"";a=b+"/"+a;b="/"===b.charAt(0)}a=Qa(a.split("/").filter(d=>!!d),!b).join("/");return(b?"/":"")+a||"."} var Va=(a,b)=>{function c(k){for(var h=0;hn?[]:k.slice(h,n-h+1)}a=Ua(a).substr(1);b=Ua(b).substr(1);a=c(a.split("/"));b=c(b.split("/"));for(var d=Math.min(a.length,b.length),f=d,g=0;g=b||(b=Math.max(b,c*(1048576>c?2:1.125)>>>0),0!=c&&(b=Math.max(b,256)),c=a.Pa,a.Pa=new Uint8Array(b),0=a.node.Va)return 0;a=Math.min(a.node.Va-f,d);if(8b)throw new N.Ma(28);return b},Ib:function(a,b,c){O.Kc(a.node,b+c);a.node.Va=Math.max(a.node.Va,b+c)},yb:function(a,b,c,d,f){if(!N.isFile(a.node.mode))throw new N.Ma(43);a=a.node.Pa;if(f&2||a.buffer!==ua){if(0{f||v('Loading data file "'+a+'" failed (no arrayBuffer).');b(new Uint8Array(f));d&&Ja(d)},()=>{if(c)c();else throw'Loading data file "'+a+'" failed.';});d&&Ia(d)} var N={root:null,Mb:[],Ic:{},streams:[],Hd:1,lb:null,Gc:"/",jc:!1,Rc:!0,Ma:null,fc:{},sd:null,Yb:0,Ua:(a,b={})=>{a=Ua(N.cwd(),a);if(!a)return{path:"",node:null};b=Object.assign({dc:!0,tc:0},b);if(8!!k),!1);for(var c=N.root,d="/",f=0;f{for(var b;;){if(N.Ub(a))return a=a.Wa.Sc,b?"/"!==a[a.length-1]?a+"/"+b:a+b:a;b=b?a.name+"/"+b:a.name;a=a.parent}},ic:(a,b)=>{for(var c=0,d=0;d>>0)%N.lb.length},Pc:a=>{var b=N.ic(a.parent.id,a.name);a.zb=N.lb[b];N.lb[b]=a},Qc:a=>{var b=N.ic(a.parent.id,a.name);if(N.lb[b]===a)N.lb[b]=a.zb;else for(b=N.lb[b];b;){if(b.zb===a){b.zb=a.zb;break}b=b.zb}},kb:(a,b)=>{var c=N.Ed(a);if(c)throw new N.Ma(c, a);for(c=N.lb[N.ic(a.id,b)];c;c=c.zb){var d=c.name;if(c.parent.id===a.id&&d===b)return c}return N.lookup(a,b)},createNode:(a,b,c,d)=>{a=new N.Vc(a,b,c,d);N.Pc(a);return a},cc:a=>{N.Qc(a)},Ub:a=>a===a.parent,wb:a=>!!a.Lb,isFile:a=>32768===(a&61440),Ya:a=>16384===(a&61440),vb:a=>40960===(a&61440),Jb:a=>8192===(a&61440),Bd:a=>24576===(a&61440),isFIFO:a=>4096===(a&61440),isSocket:a=>49152===(a&49152),td:{r:0,"r+":2,w:577,"w+":578,a:1089,"a+":1090},Gd:a=>{var b=N.td[a];if("undefined"==typeof b)throw Error("Unknown file open mode: "+ a);return b},Mc:a=>{var b=["r","w","rw"][a&3];a&512&&(b+="w");return b},sb:(a,b)=>{if(N.Rc)return 0;if(!b.includes("r")||a.mode&292){if(b.includes("w")&&!(a.mode&146)||b.includes("x")&&!(a.mode&73))return 2}else return 2;return 0},Ed:a=>{var b=N.sb(a,"x");return b?b:a.Oa.lookup?0:2},pc:(a,b)=>{try{return N.kb(a,b),20}catch(c){}return N.sb(a,"wx")},Wb:(a,b,c)=>{try{var d=N.kb(a,b)}catch(f){return f.Ra}if(a=N.sb(a,"wx"))return a;if(c){if(!N.Ya(d.mode))return 54;if(N.Ub(d)||N.qb(d)===N.cwd())return 10}else if(N.Ya(d.mode))return 31; return 0},Fd:(a,b)=>a?N.vb(a.mode)?32:N.Ya(a.mode)&&("r"!==N.Mc(b)||b&512)?31:N.sb(a,N.Mc(b)):44,Wc:4096,Id:(a=0,b=N.Wc)=>{for(;a<=b;a++)if(!N.streams[a])return a;throw new N.Ma(33);},rb:a=>N.streams[a],Fc:(a,b,c)=>{N.Pb||(N.Pb=function(){this.cb={}},N.Pb.prototype={},Object.defineProperties(N.Pb.prototype,{object:{get:function(){return this.node},set:function(d){this.node=d}},flags:{get:function(){return this.cb.flags},set:function(d){this.cb.flags=d}},position:{get:function(){return this.cb.position}, set:function(d){this.cb.position=d}}}));a=Object.assign(new N.Pb,a);b=N.Id(b,c);a.fd=b;return N.streams[b]=a},hd:a=>{N.streams[a]=null},gd:{open:a=>{a.Qa=N.wd(a.node.rdev).Qa;a.Qa.open&&a.Qa.open(a)},bb:()=>{throw new N.Ma(70);}},oc:a=>a>>8,fe:a=>a&255,xb:(a,b)=>a<<8|b,vc:(a,b)=>{N.Ic[a]={Qa:b}},wd:a=>N.Ic[a],Nc:a=>{var b=[];for(a=[a];a.length;){var c=a.pop();b.push(c);a.push.apply(a,c.Mb)}return b},Uc:(a,b)=>{function c(k){N.Yb--;return b(k)}function d(k){if(k){if(!d.qd)return d.qd=!0,c(k)}else++g>= f.length&&c(null)}"function"==typeof a&&(b=a,a=!1);N.Yb++;1{if(!k.type.Uc)return d(null);k.type.Uc(k,a,d)})},Wa:(a,b,c)=>{var d="/"===c,f=!c;if(d&&N.root)throw new N.Ma(10);if(!d&&!f){var g=N.Ua(c,{dc:!1});c=g.path;g=g.node;if(N.wb(g))throw new N.Ma(10);if(!N.Ya(g.mode))throw new N.Ma(54);}b={type:a,je:b,Sc:c,Mb:[]};a=a.Wa(b);a.Wa=b;b.root=a;d?N.root=a:g&&(g.Lb= b,g.Wa&&g.Wa.Mb.push(b));return a},ne:a=>{a=N.Ua(a,{dc:!1});if(!N.wb(a.node))throw new N.Ma(28);a=a.node;var b=a.Lb,c=N.Nc(b);Object.keys(N.lb).forEach(d=>{for(d=N.lb[d];d;){var f=d.zb;c.includes(d.Wa)&&N.cc(d);d=f}});a.Lb=null;a.Wa.Mb.splice(a.Wa.Mb.indexOf(b),1)},lookup:(a,b)=>a.Oa.lookup(a,b),ob:(a,b,c)=>{var d=N.Ua(a,{parent:!0}).node;a=M(a);if(!a||"."===a||".."===a)throw new N.Ma(28);var f=N.pc(d,a);if(f)throw new N.Ma(f);if(!d.Oa.ob)throw new N.Ma(63);return d.Oa.ob(d,a,b,c)},create:(a,b)=> N.ob(a,(void 0!==b?b:438)&4095|32768,0),mkdir:(a,b)=>N.ob(a,(void 0!==b?b:511)&1023|16384,0),ge:(a,b)=>{a=a.split("/");for(var c="",d=0;d{"undefined"==typeof c&&(c=b,b=438);return N.ob(a,b|8192,c)},symlink:(a,b)=>{if(!Ua(a))throw new N.Ma(44);var c=N.Ua(b,{parent:!0}).node;if(!c)throw new N.Ma(44);b=M(b);var d=N.pc(c,b);if(d)throw new N.Ma(d);if(!c.Oa.symlink)throw new N.Ma(63);return c.Oa.symlink(c,b, a)},rename:(a,b)=>{var c=Ra(a),d=Ra(b),f=M(a),g=M(b);var k=N.Ua(a,{parent:!0});var h=k.node;k=N.Ua(b,{parent:!0});k=k.node;if(!h||!k)throw new N.Ma(44);if(h.Wa!==k.Wa)throw new N.Ma(75);var n=N.kb(h,f);a=Va(a,d);if("."!==a.charAt(0))throw new N.Ma(28);a=Va(b,c);if("."!==a.charAt(0))throw new N.Ma(55);try{var q=N.kb(k,g)}catch(p){}if(n!==q){b=N.Ya(n.mode);if(f=N.Wb(h,f,b))throw new N.Ma(f);if(f=q?N.Wb(k,g,b):N.pc(k,g))throw new N.Ma(f);if(!h.Oa.rename)throw new N.Ma(63);if(N.wb(n)||q&&N.wb(q))throw new N.Ma(10); if(k!==h&&(f=N.sb(h,"w")))throw new N.Ma(f);N.Qc(n);try{h.Oa.rename(n,k,g)}catch(p){throw p;}finally{N.Pc(n)}}},rmdir:a=>{var b=N.Ua(a,{parent:!0}).node;a=M(a);var c=N.kb(b,a),d=N.Wb(b,a,!0);if(d)throw new N.Ma(d);if(!b.Oa.rmdir)throw new N.Ma(63);if(N.wb(c))throw new N.Ma(10);b.Oa.rmdir(b,a);N.cc(c)},readdir:a=>{a=N.Ua(a,{eb:!0}).node;if(!a.Oa.readdir)throw new N.Ma(54);return a.Oa.readdir(a)},unlink:a=>{var b=N.Ua(a,{parent:!0}).node;if(!b)throw new N.Ma(44);a=M(a);var c=N.kb(b,a),d=N.Wb(b,a,!1); if(d)throw new N.Ma(d);if(!b.Oa.unlink)throw new N.Ma(63);if(N.wb(c))throw new N.Ma(10);b.Oa.unlink(b,a);N.cc(c)},readlink:a=>{a=N.Ua(a).node;if(!a)throw new N.Ma(44);if(!a.Oa.readlink)throw new N.Ma(28);return Ua(N.qb(a.parent),a.Oa.readlink(a))},stat:(a,b)=>{a=N.Ua(a,{eb:!b}).node;if(!a)throw new N.Ma(44);if(!a.Oa.jb)throw new N.Ma(63);return a.Oa.jb(a)},lstat:a=>N.stat(a,!0),chmod:(a,b,c)=>{a="string"==typeof a?N.Ua(a,{eb:!c}).node:a;if(!a.Oa.Za)throw new N.Ma(63);a.Oa.Za(a,{mode:b&4095|a.mode& -4096,timestamp:Date.now()})},lchmod:(a,b)=>{N.chmod(a,b,!0)},fchmod:(a,b)=>{a=N.rb(a);if(!a)throw new N.Ma(8);N.chmod(a.node,b)},chown:(a,b,c,d)=>{a="string"==typeof a?N.Ua(a,{eb:!d}).node:a;if(!a.Oa.Za)throw new N.Ma(63);a.Oa.Za(a,{timestamp:Date.now()})},lchown:(a,b,c)=>{N.chown(a,b,c,!0)},fchown:(a,b,c)=>{a=N.rb(a);if(!a)throw new N.Ma(8);N.chown(a.node,b,c)},truncate:(a,b)=>{if(0>b)throw new N.Ma(28);a="string"==typeof a?N.Ua(a,{eb:!0}).node:a;if(!a.Oa.Za)throw new N.Ma(63);if(N.Ya(a.mode))throw new N.Ma(31); if(!N.isFile(a.mode))throw new N.Ma(28);var c=N.sb(a,"w");if(c)throw new N.Ma(c);a.Oa.Za(a,{size:b,timestamp:Date.now()})},ud:(a,b)=>{a=N.rb(a);if(!a)throw new N.Ma(8);if(0===(a.flags&2097155))throw new N.Ma(28);N.truncate(a.node,b)},ae:(a,b,c)=>{a=N.Ua(a,{eb:!0}).node;a.Oa.Za(a,{timestamp:Math.max(b,c)})},open:(a,b,c)=>{if(""===a)throw new N.Ma(44);b="string"==typeof b?N.Gd(b):b;c=b&64?("undefined"==typeof c?438:c)&4095|32768:0;if("object"==typeof a)var d=a;else{a=L(a);try{d=N.Ua(a,{eb:!(b&131072)}).node}catch(g){}}var f= !1;if(b&64)if(d){if(b&128)throw new N.Ma(20);}else d=N.ob(a,c,0),f=!0;if(!d)throw new N.Ma(44);N.Jb(d.mode)&&(b&=-513);if(b&65536&&!N.Ya(d.mode))throw new N.Ma(54);if(!f&&(c=N.Fd(d,b)))throw new N.Ma(c);b&512&&!f&&N.truncate(d,0);b&=-131713;d=N.Fc({node:d,path:N.qb(d),flags:b,seekable:!0,position:0,Qa:d.Qa,$d:[],error:!1});d.Qa.open&&d.Qa.open(d);!e.logReadFiles||b&1||(N.sc||(N.sc={}),a in N.sc||(N.sc[a]=1));return d},close:a=>{if(N.Kb(a))throw new N.Ma(8);a.ub&&(a.ub=null);try{a.Qa.close&&a.Qa.close(a)}catch(b){throw b; }finally{N.hd(a.fd)}a.fd=null},Kb:a=>null===a.fd,bb:(a,b,c)=>{if(N.Kb(a))throw new N.Ma(8);if(!a.seekable||!a.Qa.bb)throw new N.Ma(70);if(0!=c&&1!=c&&2!=c)throw new N.Ma(28);a.position=a.Qa.bb(a,b,c);a.$d=[];return a.position},read:(a,b,c,d,f)=>{if(0>d||0>f)throw new N.Ma(28);if(N.Kb(a))throw new N.Ma(8);if(1===(a.flags&2097155))throw new N.Ma(8);if(N.Ya(a.node.mode))throw new N.Ma(31);if(!a.Qa.read)throw new N.Ma(28);var g="undefined"!=typeof f;if(!g)f=a.position;else if(!a.seekable)throw new N.Ma(70); b=a.Qa.read(a,b,c,d,f);g||(a.position+=b);return b},write:(a,b,c,d,f,g)=>{if(0>d||0>f)throw new N.Ma(28);if(N.Kb(a))throw new N.Ma(8);if(0===(a.flags&2097155))throw new N.Ma(8);if(N.Ya(a.node.mode))throw new N.Ma(31);if(!a.Qa.write)throw new N.Ma(28);a.seekable&&a.flags&1024&&N.bb(a,0,2);var k="undefined"!=typeof f;if(!k)f=a.position;else if(!a.seekable)throw new N.Ma(70);b=a.Qa.write(a,b,c,d,f,g);k||(a.position+=b);return b},Ib:(a,b,c)=>{if(N.Kb(a))throw new N.Ma(8);if(0>b||0>=c)throw new N.Ma(28); if(0===(a.flags&2097155))throw new N.Ma(8);if(!N.isFile(a.node.mode)&&!N.Ya(a.node.mode))throw new N.Ma(43);if(!a.Qa.Ib)throw new N.Ma(138);a.Qa.Ib(a,b,c)},yb:(a,b,c,d,f)=>{if(0!==(d&2)&&0===(f&2)&&2!==(a.flags&2097155))throw new N.Ma(2);if(1===(a.flags&2097155))throw new N.Ma(2);if(!a.Qa.yb)throw new N.Ma(43);return a.Qa.yb(a,b,c,d,f)},Fb:(a,b,c,d,f)=>a&&a.Qa.Fb?a.Qa.Fb(a,b,c,d,f):0,he:()=>0,kc:(a,b,c)=>{if(!a.Qa.kc)throw new N.Ma(59);return a.Qa.kc(a,b,c)},readFile:(a,b={})=>{b.flags=b.flags||0; b.encoding=b.encoding||"binary";if("utf8"!==b.encoding&&"binary"!==b.encoding)throw Error('Invalid encoding type "'+b.encoding+'"');var c,d=N.open(a,b.flags);a=N.stat(a).size;var f=new Uint8Array(a);N.read(d,f,0,a,0);"utf8"===b.encoding?c=sa(f,0):"binary"===b.encoding&&(c=f);N.close(d);return c},writeFile:(a,b,c={})=>{c.flags=c.flags||577;a=N.open(a,c.flags,c.mode);if("string"==typeof b){var d=new Uint8Array(ta(b)+1);b=A(b,d,0,d.length);N.write(a,d,0,b,void 0,c.ed)}else if(ArrayBuffer.isView(b))N.write(a, b,0,b.byteLength,void 0,c.ed);else throw Error("Unsupported data type");N.close(a)},cwd:()=>N.Gc,chdir:a=>{a=N.Ua(a,{eb:!0});if(null===a.node)throw new N.Ma(44);if(!N.Ya(a.node.mode))throw new N.Ma(54);var b=N.sb(a.node,"x");if(b)throw new N.Ma(b);N.Gc=a.path},ld:()=>{N.mkdir("/tmp");N.mkdir("/home");N.mkdir("/home/web_user")},kd:()=>{N.mkdir("/dev");N.vc(N.xb(1,3),{read:()=>0,write:(b,c,d,f)=>f});N.Xb("/dev/null",N.xb(1,3));Ya(N.xb(5,0),$a);Ya(N.xb(6,0),ab);N.Xb("/dev/tty",N.xb(5,0));N.Xb("/dev/tty1", N.xb(6,0));var a=Ta();N.hb("/dev","random",a);N.hb("/dev","urandom",a);N.mkdir("/dev/shm");N.mkdir("/dev/shm/tmp")},nd:()=>{N.mkdir("/proc");var a=N.mkdir("/proc/self");N.mkdir("/proc/self/fd");N.Wa({Wa:()=>{var b=N.createNode(a,"fd",16895,73);b.Oa={lookup:(c,d)=>{var f=N.rb(+d);if(!f)throw new N.Ma(8);c={parent:null,Wa:{Sc:"fake"},Oa:{readlink:()=>f.path}};return c.parent=c}};return b}},{},"/proc/self/fd")},od:()=>{e.stdin?N.hb("/dev","stdin",e.stdin):N.symlink("/dev/tty","/dev/stdin");e.stdout? N.hb("/dev","stdout",null,e.stdout):N.symlink("/dev/tty","/dev/stdout");e.stderr?N.hb("/dev","stderr",null,e.stderr):N.symlink("/dev/tty1","/dev/stderr");N.open("/dev/stdin",0);N.open("/dev/stdout",1);N.open("/dev/stderr",1)},Jc:()=>{N.Ma||(N.Ma=function(a,b){this.node=b;this.Qd=function(c){this.Ra=c};this.Qd(a);this.message="FS error"},N.Ma.prototype=Error(),N.Ma.prototype.constructor=N.Ma,[44].forEach(a=>{N.fc[a]=new N.Ma(a);N.fc[a].stack=""}))},Vd:()=>{N.Jc();N.lb=Array(4096); N.Wa(O,{},"/");N.ld();N.kd();N.nd();N.sd={MEMFS:O}},Db:(a,b,c)=>{N.Db.jc=!0;N.Jc();e.stdin=a||e.stdin;e.stdout=b||e.stdout;e.stderr=c||e.stderr;N.od()},ke:()=>{N.Db.jc=!1;for(var a=0;a{var c=0;a&&(c|=365);b&&(c|=146);return c},be:(a,b)=>{a=N.bc(a,b);return a.exists?a.object:null},bc:(a,b)=>{try{var c=N.Ua(a,{eb:!b});a=c.path}catch(f){}var d={Ub:!1,exists:!1,error:0,name:null,path:null,object:null,Jd:!1,Ld:null,Kd:null};try{c=N.Ua(a, {parent:!0}),d.Jd=!0,d.Ld=c.path,d.Kd=c.node,d.name=M(a),c=N.Ua(a,{eb:!b}),d.exists=!0,d.path=c.path,d.object=c.node,d.name=c.node.name,d.Ub="/"===c.path}catch(f){d.error=f.Ra}return d},Dc:(a,b)=>{a="string"==typeof a?a:N.qb(a);for(b=b.split("/").reverse();b.length;){var c=b.pop();if(c){var d=L(a+"/"+c);try{N.mkdir(d)}catch(f){}a=d}}return d},md:(a,b,c,d,f)=>{a="string"==typeof a?a:N.qb(a);b=L(a+"/"+b);return N.create(b,N.hc(d,f))},Rb:(a,b,c,d,f,g)=>{var k=b;a&&(a="string"==typeof a?a:N.qb(a),k=b? L(a+"/"+b):a);a=N.hc(d,f);k=N.create(k,a);if(c){if("string"==typeof c){b=Array(c.length);d=0;for(f=c.length;d{a=Sa("string"==typeof a?a:N.qb(a),b);b=N.hc(!!c,!!d);N.hb.oc||(N.hb.oc=64);var f=N.xb(N.hb.oc++,0);N.vc(f,{open:g=>{g.seekable=!1},close:()=>{d&&d.buffer&&d.buffer.length&&d(10)},read:(g,k,h,n)=>{for(var q=0,p=0;p{for(var q=0;q{if(a.lc||a.Cd||a.link||a.Pa)return!0;if("undefined"!=typeof XMLHttpRequest)throw Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); if(ia)try{a.Pa=Wa(ia(a.url),!0),a.Va=a.Pa.length}catch(b){throw new N.Ma(29);}else throw Error("Cannot load without read() or XMLHttpRequest.");},Cc:(a,b,c,d,f)=>{function g(){this.nc=!1;this.cb=[]}function k(p,t,x,l,u){p=p.node.Pa;if(u>=p.length)return 0;l=Math.min(p.length-u,l);if(p.slice)for(var w=0;wthis.length-1||0>p)){var t=p%this.chunkSize;return this.Sb(p/this.chunkSize|0)[t]}};g.prototype.ac= function(p){this.Sb=p};g.prototype.Ac=function(){var p=new XMLHttpRequest;p.open("HEAD",c,!1);p.send(null);if(!(200<=p.status&&300>p.status||304===p.status))throw Error("Couldn't load "+c+". Status: "+p.status);var t=Number(p.getResponseHeader("Content-length")),x,l=(x=p.getResponseHeader("Accept-Ranges"))&&"bytes"===x;p=(x=p.getResponseHeader("Content-Encoding"))&&"gzip"===x;var u=1048576;l||(u=t);var w=this;w.ac(C=>{var K=C*u,H=(C+1)*u-1;H=Math.min(H,t-1);if("undefined"==typeof w.cb[C]){var Ga= w.cb;if(K>H)throw Error("invalid range ("+K+", "+H+") or no bytes requested!");if(H>t-1)throw Error("only "+t+" bytes available! programmer error!");var I=new XMLHttpRequest;I.open("GET",c,!1);t!==u&&I.setRequestHeader("Range","bytes="+K+"-"+H);I.responseType="arraybuffer";I.overrideMimeType&&I.overrideMimeType("text/plain; charset=x-user-defined");I.send(null);if(!(200<=I.status&&300>I.status||304===I.status))throw Error("Couldn't load "+c+". Status: "+I.status);K=void 0!==I.response?new Uint8Array(I.response|| []):Wa(I.responseText||"",!0);Ga[C]=K}if("undefined"==typeof w.cb[C])throw Error("doXHR failed!");return w.cb[C]});if(p||!t)u=t=1,u=t=this.Sb(0).length,na("LazyFiles on gzip forces download of the whole file when length is accessed");this.bd=t;this.ad=u;this.nc=!0};if("undefined"!=typeof XMLHttpRequest){if(!fa)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var h=new g;Object.defineProperties(h,{length:{get:function(){this.nc|| this.Ac();return this.bd}},chunkSize:{get:function(){this.nc||this.Ac();return this.ad}}});h={lc:!1,Pa:h}}else h={lc:!1,url:c};var n=N.md(a,b,h,d,f);h.Pa?n.Pa=h.Pa:h.url&&(n.Pa=null,n.url=h.url);Object.defineProperties(n,{Va:{get:function(){return this.Pa.length}}});var q={};Object.keys(n.Qa).forEach(p=>{var t=n.Qa[p];q[p]=function(){N.ec(n);return t.apply(null,arguments)}});q.read=(p,t,x,l,u)=>{N.ec(n);return k(p,t,x,l,u)};q.yb=(p,t,x)=>{N.ec(n);var l=bb(t);if(!l)throw new N.Ma(48);k(p,B,l,t,x); return{Sa:l,zc:!0}};n.Qa=q;return n},Ec:(a,b,c,d,f,g,k,h,n,q)=>{function p(l){function u(w){q&&q();h||N.Rb(a,b,w,d,f,n);g&&g();Ja(x)}eb.de(l,t,u,()=>{k&&k();Ja(x)})||u(l)}var t=b?Ua(L(a+"/"+b)):a,x="cp "+t;Ia(x);"string"==typeof c?db(c,l=>p(l),k):p(c)},indexedDB:()=>window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB,xc:()=>"EM_FS_"+window.location.pathname,yc:20,Hb:"FILE_DATA",le:(a,b,c)=>{b=b||(()=>{});c=c||(()=>{});var d=N.indexedDB();try{var f=d.open(N.xc(),N.yc)}catch(g){return c(g)}f.onupgradeneeded= ()=>{na("creating db");f.result.createObjectStore(N.Hb)};f.onsuccess=()=>{var g=f.result.transaction([N.Hb],"readwrite"),k=g.objectStore(N.Hb),h=0,n=0,q=a.length;a.forEach(p=>{p=k.put(N.bc(p).object.Pa,p);p.onsuccess=()=>{h++;h+n==q&&(0==n?b():c())};p.onerror=()=>{n++;h+n==q&&(0==n?b():c())}});g.onerror=c};f.onerror=c},ee:(a,b,c)=>{b=b||(()=>{});c=c||(()=>{});var d=N.indexedDB();try{var f=d.open(N.xc(),N.yc)}catch(g){return c(g)}f.onupgradeneeded=c;f.onsuccess=()=>{var g=f.result;try{var k=g.transaction([N.Hb], "readonly")}catch(t){c(t);return}var h=k.objectStore(N.Hb),n=0,q=0,p=a.length;a.forEach(t=>{var x=h.get(t);x.onsuccess=()=>{N.bc(t).exists&&N.unlink(t);N.Rb(Ra(t),M(t),x.result,!0,!0,!0);n++;n+q==p&&(0==q?b():c())};x.onerror=()=>{q++;n+q==p&&(0==q?b():c())}});k.onerror=c};f.onerror=c}};function P(a,b,c){if("/"===b.charAt(0))return b;if(-100===a)a=N.cwd();else{a=N.rb(a);if(!a)throw new N.Ma(8);a=a.path}if(0==b.length){if(!c)throw new N.Ma(44);return a}return L(a+"/"+b)} function fb(a,b,c){try{var d=a(b)}catch(f){if(f&&f.node&&L(b)!==L(N.qb(f.node)))return-54;throw f;}E[c>>2]=d.dev;E[c+4>>2]=0;E[c+8>>2]=d.ino;E[c+12>>2]=d.mode;E[c+16>>2]=d.nlink;E[c+20>>2]=d.uid;E[c+24>>2]=d.gid;E[c+28>>2]=d.rdev;E[c+32>>2]=0;J=[d.size>>>0,(G=d.size,1<=+Math.abs(G)?0>>0:~~+Math.ceil((G-+(~~G>>>0))/4294967296)>>>0:0)];E[c+40>>2]=J[0];E[c+44>>2]=J[1];E[c+48>>2]=4096;E[c+52>>2]=d.blocks;J=[Math.floor(d.atime.getTime()/1E3)>>>0,(G= Math.floor(d.atime.getTime()/1E3),1<=+Math.abs(G)?0>>0:~~+Math.ceil((G-+(~~G>>>0))/4294967296)>>>0:0)];E[c+56>>2]=J[0];E[c+60>>2]=J[1];E[c+64>>2]=0;J=[Math.floor(d.mtime.getTime()/1E3)>>>0,(G=Math.floor(d.mtime.getTime()/1E3),1<=+Math.abs(G)?0>>0:~~+Math.ceil((G-+(~~G>>>0))/4294967296)>>>0:0)];E[c+72>>2]=J[0];E[c+76>>2]=J[1];E[c+80>>2]=0;J=[Math.floor(d.ctime.getTime()/1E3)>>>0,(G=Math.floor(d.ctime.getTime()/ 1E3),1<=+Math.abs(G)?0>>0:~~+Math.ceil((G-+(~~G>>>0))/4294967296)>>>0:0)];E[c+88>>2]=J[0];E[c+92>>2]=J[1];E[c+96>>2]=0;J=[d.ino>>>0,(G=d.ino,1<=+Math.abs(G)?0>>0:~~+Math.ceil((G-+(~~G>>>0))/4294967296)>>>0:0)];E[c+104>>2]=J[0];E[c+108>>2]=J[1];return 0}var gb=void 0;function hb(){gb+=4;return E[gb-4>>2]}function Q(a){a=N.rb(a);if(!a)throw new N.Ma(8);return a} function ib(a){return F[a>>2]+4294967296*E[a+4>>2]}var jb={};function kb(a){for(;a.length;){var b=a.pop();a.pop()(b)}}function lb(a){return this.fromWireType(E[a>>2])}var mb={},nb={},ob={};function pb(a){if(void 0===a)return"_unknown";a=a.replace(/[^a-zA-Z0-9_]/g,"$");var b=a.charCodeAt(0);return 48<=b&&57>=b?"_"+a:a}function qb(a,b){a=pb(a);return(new Function("body","return function "+a+'() {\n "use strict"; return body.apply(this, arguments);\n};\n'))(b)} function rb(a){var b=Error,c=qb(a,function(d){this.name=a;this.message=d;d=Error(d).stack;void 0!==d&&(this.stack=this.toString()+"\n"+d.replace(/^Error(:[^\n]*)?\n/,""))});c.prototype=Object.create(b.prototype);c.prototype.constructor=c;c.prototype.toString=function(){return void 0===this.message?this.name:this.name+": "+this.message};return c}var sb=void 0;function tb(a){throw new sb(a);} function ub(a,b,c){function d(h){h=c(h);h.length!==a.length&&tb("Mismatched type converter count");for(var n=0;n{nb.hasOwnProperty(h)?f[n]=nb[h]:(g.push(h),mb.hasOwnProperty(h)||(mb[h]=[]),mb[h].push(()=>{f[n]=nb[h];++k;k===g.length&&d(f)}))});0===g.length&&d(f)} function vb(a){switch(a){case 1:return 0;case 2:return 1;case 4:return 2;case 8:return 3;default:throw new TypeError("Unknown type size: "+a);}}var wb=void 0;function S(a){for(var b="";z[a];)b+=wb[z[a++]];return b}var xb=void 0;function T(a){throw new xb(a);} function R(a,b,c={}){if(!("argPackAdvance"in b))throw new TypeError("registerType registeredInstance requires argPackAdvance");var d=b.name;a||T('type "'+d+'" must have a positive integer typeid pointer');if(nb.hasOwnProperty(a)){if(c.Ad)return;T("Cannot register type '"+d+"' twice")}nb[a]=b;delete ob[a];mb.hasOwnProperty(a)&&(b=mb[a],delete mb[a],b.forEach(f=>f()))}function yb(a){T(a.Na.Xa.Ta.name+" instance already deleted")}var zb=!1;function Ab(){} function Bb(a){--a.count.value;0===a.count.value&&(a.ab?a.fb.pb(a.ab):a.Xa.Ta.pb(a.Sa))}function Cb(a,b,c){if(b===c)return a;if(void 0===c.gb)return null;a=Cb(a,b,c.gb);return null===a?null:c.pd(a)}var Db={},Eb=[];function Fb(){for(;Eb.length;){var a=Eb.pop();a.Na.Cb=!1;a["delete"]()}}var Gb=void 0,Hb={};function Ib(a,b){for(void 0===b&&T("ptr should not be undefined");a.gb;)b=a.Ob(b),a=a.gb;return Hb[b]} function Jb(a,b){b.Xa&&b.Sa||tb("makeClassHandle requires ptr and ptrType");!!b.fb!==!!b.ab&&tb("Both smartPtrType and smartPtr must be specified");b.count={value:1};return Kb(Object.create(a,{Na:{value:b}}))}function Kb(a){if("undefined"===typeof FinalizationRegistry)return Kb=b=>b,a;zb=new FinalizationRegistry(b=>{Bb(b.Na)});Kb=b=>{var c=b.Na;c.ab&&zb.register(b,{Na:c},b);return b};Ab=b=>{zb.unregister(b)};return Kb(a)}function U(){} function Lb(a,b,c){if(void 0===a[b].$a){var d=a[b];a[b]=function(){a[b].$a.hasOwnProperty(arguments.length)||T("Function '"+c+"' called with an invalid number of arguments ("+arguments.length+") - expects one of ("+a[b].$a+")!");return a[b].$a[arguments.length].apply(this,arguments)};a[b].$a=[];a[b].$a[d.Qb]=d}} function Mb(a,b,c){e.hasOwnProperty(a)?((void 0===c||void 0!==e[a].$a&&void 0!==e[a].$a[c])&&T("Cannot register public name '"+a+"' twice"),Lb(e,a,a),e.hasOwnProperty(c)&&T("Cannot register multiple overloads of a function with the same number of arguments ("+c+")!"),e[a].$a[c]=b):(e[a]=b,void 0!==c&&(e[a].ie=c))}function Nb(a,b,c,d,f,g,k,h){this.name=a;this.constructor=b;this.Eb=c;this.pb=d;this.gb=f;this.vd=g;this.Ob=k;this.pd=h;this.Nd=[]} function Ob(a,b,c){for(;b!==c;)b.Ob||T("Expected null or instance of "+c.name+", got an instance of "+b.name),a=b.Ob(a),b=b.gb;return a}function Pb(a,b){if(null===b)return this.mc&&T("null is not a valid "+this.name),0;b.Na||T('Cannot pass "'+Qb(b)+'" as a '+this.name);b.Na.Sa||T("Cannot pass deleted object as a pointer of type "+this.name);return Ob(b.Na.Sa,b.Na.Xa.Ta,this.Ta)} function Rb(a,b){if(null===b){this.mc&&T("null is not a valid "+this.name);if(this.Vb){var c=this.rc();null!==a&&a.push(this.pb,c);return c}return 0}b.Na||T('Cannot pass "'+Qb(b)+'" as a '+this.name);b.Na.Sa||T("Cannot pass deleted object as a pointer of type "+this.name);!this.Tb&&b.Na.Xa.Tb&&T("Cannot convert argument of type "+(b.Na.fb?b.Na.fb.name:b.Na.Xa.name)+" to parameter type "+this.name);c=Ob(b.Na.Sa,b.Na.Xa.Ta,this.Ta);if(this.Vb)switch(void 0===b.Na.ab&&T("Passing raw pointer to smart pointer is illegal"), this.Ud){case 0:b.Na.fb===this?c=b.Na.ab:T("Cannot convert argument of type "+(b.Na.fb?b.Na.fb.name:b.Na.Xa.name)+" to parameter type "+this.name);break;case 1:c=b.Na.ab;break;case 2:if(b.Na.fb===this)c=b.Na.ab;else{var d=b.clone();c=this.Od(c,Sb(function(){d["delete"]()}));null!==a&&a.push(this.pb,c)}break;default:T("Unsupporting sharing policy")}return c} function Tb(a,b){if(null===b)return this.mc&&T("null is not a valid "+this.name),0;b.Na||T('Cannot pass "'+Qb(b)+'" as a '+this.name);b.Na.Sa||T("Cannot pass deleted object as a pointer of type "+this.name);b.Na.Xa.Tb&&T("Cannot convert argument of type "+b.Na.Xa.name+" to parameter type "+this.name);return Ob(b.Na.Sa,b.Na.Xa.Ta,this.Ta)} function V(a,b,c,d){this.name=a;this.Ta=b;this.mc=c;this.Tb=d;this.Vb=!1;this.pb=this.Od=this.rc=this.Tc=this.Ud=this.Md=void 0;void 0!==b.gb?this.toWireType=Rb:(this.toWireType=d?Pb:Tb,this.ib=null)}function Ub(a,b,c){e.hasOwnProperty(a)||tb("Replacing nonexistant public symbol");void 0!==e[a].$a&&void 0!==c?e[a].$a[c]=b:(e[a]=b,e[a].Qb=c)} function Vb(a,b){var c=[];return function(){c.length=0;Object.assign(c,arguments);if(a.includes("j")){var d=e["dynCall_"+a];d=c&&c.length?d.apply(null,[b].concat(c)):d.call(null,b)}else d=za.get(b).apply(null,c);return d}}function W(a,b){a=S(a);var c=a.includes("j")?Vb(a,b):za.get(b);"function"!=typeof c&&T("unknown function pointer with signature "+a+": "+b);return c}var Wb=void 0;function Xb(a){a=Yb(a);var b=S(a);X(a);return b} function Zb(a,b){function c(g){f[g]||nb[g]||(ob[g]?ob[g].forEach(c):(d.push(g),f[g]=!0))}var d=[],f={};b.forEach(c);throw new Wb(a+": "+d.map(Xb).join([", "]));}function $b(a,b){for(var c=[],d=0;d>2]);return c} function ac(a){var b=Function;if(!(b instanceof Function))throw new TypeError("new_ called with constructor type "+typeof b+" which is not a function");var c=qb(b.name||"unknownFunctionName",function(){});c.prototype=b.prototype;c=new c;a=b.apply(c,a);return a instanceof Object?a:c} function bc(a,b,c,d,f){var g=b.length;2>g&&T("argTypes array size mismatch! Must at least get return value and 'this' types!");var k=null!==b[1]&&null!==c,h=!1;for(c=1;c{a||T("Cannot use deleted val. handle = "+a);return Y[a].value},Sb=a=>{switch(a){case void 0:return 1;case null:return 2;case !0:return 3;case !1:return 4;default:var b=cc.length?cc.pop():Y.length;Y[b]={uc:1,value:a};return b}};function Qb(a){if(null===a)return"null";var b=typeof a;return"object"===b||"array"===b||"function"===b?a.toString():""+a} function fc(a,b){switch(b){case 2:return function(c){return this.fromWireType(wa[c>>2])};case 3:return function(c){return this.fromWireType(xa[c>>3])};default:throw new TypeError("Unknown float type: "+a);}} function gc(a,b,c){switch(b){case 0:return c?function(d){return B[d]}:function(d){return z[d]};case 1:return c?function(d){return D[d>>1]}:function(d){return va[d>>1]};case 2:return c?function(d){return E[d>>2]}:function(d){return F[d>>2]};default:throw new TypeError("Unknown integer type: "+a);}}var hc="undefined"!=typeof TextDecoder?new TextDecoder("utf-16le"):void 0; function ic(a,b){var c=a>>1;for(var d=c+b/2;!(c>=d)&&va[c];)++c;c<<=1;if(32=b/2);++d){var f=D[a+2*d>>1];if(0==f)break;c+=String.fromCharCode(f)}return c}function jc(a,b,c){void 0===c&&(c=2147483647);if(2>c)return 0;c-=2;var d=b;c=c<2*a.length?c/2:a.length;for(var f=0;f>1]=a.charCodeAt(f),b+=2;D[b>>1]=0;return b-d}function kc(a){return 2*a.length} function lc(a,b){for(var c=0,d="";!(c>=b/4);){var f=E[a+4*c>>2];if(0==f)break;++c;65536<=f?(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|f&1023)):d+=String.fromCharCode(f)}return d}function mc(a,b,c){void 0===c&&(c=2147483647);if(4>c)return 0;var d=b;c=d+c-4;for(var f=0;f=g){var k=a.charCodeAt(++f);g=65536+((g&1023)<<10)|k&1023}E[b>>2]=g;b+=4;if(b+4>c)break}E[b>>2]=0;return b-d} function nc(a){for(var b=0,c=0;c=d&&++c;b+=4}return b}function oc(a,b){var c=nb[a];void 0===c&&T(b+" has unknown type "+Xb(a));return c}var pc={};function qc(a){var b=pc[a];return void 0===b?S(a):b}var rc=[];function sc(a){var b=rc.length;rc.push(a);return b}function tc(a,b){for(var c=Array(a),d=0;d>2],"parameter "+d);return c}var uc=[];function vc(a){var b=ta(a)+1,c=wc(b);c&&A(a,B,c,b);return c} function xc(a,b,c){function d(n){return(n=n.toTimeString().match(/\(([A-Za-z ]+)\)$/))?n[1]:"GMT"}var f=(new Date).getFullYear(),g=new Date(f,0,1),k=new Date(f,6,1);f=g.getTimezoneOffset();var h=k.getTimezoneOffset();E[a>>2]=60*Math.max(f,h);E[b>>2]=Number(f!=h);a=d(g);b=d(k);a=vc(a);b=vc(b);h>2]=a,F[c+4>>2]=b):(F[c>>2]=b,F[c+4>>2]=a)}function yc(a,b,c){yc.dd||(yc.dd=!0,xc(a,b,c))}var zc;zc=ha?()=>{var a=process.hrtime();return 1E3*a[0]+a[1]/1E6}:()=>performance.now();var Ac={}; function Bc(){if(!Cc){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:da||"./this.program"},b;for(b in Ac)void 0===Ac[b]?delete a[b]:a[b]=Ac[b];var c=[];for(b in a)c.push(b+"="+a[b]);Cc=c}return Cc}var Cc;function Dc(a){return 0===a%4&&(0!==a%100||0===a%400)}var Ec=[31,29,31,30,31,30,31,31,30,31,30,31],Fc=[31,28,31,30,31,30,31,31,30,31,30,31]; function Gc(a,b,c,d){function f(l,u,w){for(l="number"==typeof l?l.toString():l||"";l.lengthK?-1:0C-l.getDate())u-=C-l.getDate()+1,l.setDate(1),11>w?l.setMonth(w+1):(l.setMonth(0),l.setFullYear(l.getFullYear()+1));else{l.setDate(l.getDate()+u);break}}w=new Date(l.getFullYear()+1,0,4);u=h(new Date(l.getFullYear(), 0,4));w=h(w);return 0>=k(u,l)?0>=k(w,l)?l.getFullYear()+1:l.getFullYear():l.getFullYear()-1}var q=E[d+40>>2];d={Yd:E[d>>2],Xd:E[d+4>>2],Zb:E[d+8>>2],wc:E[d+12>>2],$b:E[d+16>>2],Bb:E[d+20>>2],nb:E[d+24>>2],Ab:E[d+28>>2],me:E[d+32>>2],Wd:E[d+36>>2],Zd:q?y(q):""};c=y(c);q={"%c":"%a %b %d %H:%M:%S %Y","%D":"%m/%d/%y","%F":"%Y-%m-%d","%h":"%b","%r":"%I:%M:%S %p","%R":"%H:%M","%T":"%H:%M:%S","%x":"%m/%d/%y","%X":"%H:%M:%S","%Ec":"%c","%EC":"%C","%Ex":"%m/%d/%y","%EX":"%H:%M:%S","%Ey":"%y","%EY":"%Y","%Od":"%d", "%Oe":"%e","%OH":"%H","%OI":"%I","%Om":"%m","%OM":"%M","%OS":"%S","%Ou":"%u","%OU":"%U","%OV":"%V","%Ow":"%w","%OW":"%W","%Oy":"%y"};for(var p in q)c=c.replace(new RegExp(p,"g"),q[p]);var t="Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),x="January February March April May June July August September October November December".split(" ");q={"%a":function(l){return t[l.nb].substring(0,3)},"%A":function(l){return t[l.nb]},"%b":function(l){return x[l.$b].substring(0,3)},"%B":function(l){return x[l.$b]}, "%C":function(l){return g((l.Bb+1900)/100|0,2)},"%d":function(l){return g(l.wc,2)},"%e":function(l){return f(l.wc,2," ")},"%g":function(l){return n(l).toString().substring(2)},"%G":function(l){return n(l)},"%H":function(l){return g(l.Zb,2)},"%I":function(l){l=l.Zb;0==l?l=12:12l.Zb?"AM":"PM"},"%S":function(l){return g(l.Yd,2)},"%t":function(){return"\t"},"%u":function(l){return l.nb||7},"%U":function(l){return g(Math.floor((l.Ab+7-l.nb)/7),2)},"%V":function(l){var u=Math.floor((l.Ab+7-(l.nb+6)%7)/7);2>=(l.nb+371-l.Ab-2)%7&&u++;if(u)53==u&&(w=(l.nb+371-l.Ab)%7,4==w||3==w&&Dc(l.Bb)||(u=1));else{u=52;var w=(l.nb+7-l.Ab-1)%7;(4==w||5==w&&Dc(l.Bb%400-1))&&u++}return g(u,2)},"%w":function(l){return l.nb},"%W":function(l){return g(Math.floor((l.Ab+7-(l.nb+6)%7)/7),2)}, "%y":function(l){return(l.Bb+1900).toString().substring(2)},"%Y":function(l){return l.Bb+1900},"%z":function(l){l=l.Wd;var u=0<=l;l=Math.abs(l)/60;return(u?"+":"-")+String("0000"+(l/60*100+l%60)).slice(-4)},"%Z":function(l){return l.Zd},"%%":function(){return"%"}};c=c.replace(/%%/g,"\x00\x00");for(p in q)c.includes(p)&&(c=c.replace(new RegExp(p,"g"),q[p](d)));c=c.replace(/\0\0/g,"%");p=Wa(c,!1);if(p.length>b)return 0;B.set(p,a);return p.length-1} function Hc(a,b,c,d){a||(a=this);this.parent=a;this.Wa=a.Wa;this.Lb=null;this.id=N.Hd++;this.name=b;this.mode=c;this.Oa={};this.Qa={};this.rdev=d}Object.defineProperties(Hc.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}},Cd:{get:function(){return N.Ya(this.mode)}},lc:{get:function(){return N.Jb(this.mode)}}});N.Vc=Hc;N.Vd();var eb; e.FS_createPath=N.Dc;e.FS_createDataFile=N.Rb;e.FS_createPreloadedFile=N.Ec;e.FS_unlink=N.unlink;e.FS_createLazyFile=N.Cc;e.FS_createDevice=N.hb;sb=e.InternalError=rb("InternalError");for(var Ic=Array(256),Jc=0;256>Jc;++Jc)Ic[Jc]=String.fromCharCode(Jc);wb=Ic;xb=e.BindingError=rb("BindingError"); U.prototype.isAliasOf=function(a){if(!(this instanceof U&&a instanceof U))return!1;var b=this.Na.Xa.Ta,c=this.Na.Sa,d=a.Na.Xa.Ta;for(a=a.Na.Sa;b.gb;)c=b.Ob(c),b=b.gb;for(;d.gb;)a=d.Ob(a),d=d.gb;return b===d&&c===a}; U.prototype.clone=function(){this.Na.Sa||yb(this);if(this.Na.Nb)return this.Na.count.value+=1,this;var a=Kb,b=Object,c=b.create,d=Object.getPrototypeOf(this),f=this.Na;a=a(c.call(b,d,{Na:{value:{count:f.count,Cb:f.Cb,Nb:f.Nb,Sa:f.Sa,Xa:f.Xa,ab:f.ab,fb:f.fb}}}));a.Na.count.value+=1;a.Na.Cb=!1;return a};U.prototype["delete"]=function(){this.Na.Sa||yb(this);this.Na.Cb&&!this.Na.Nb&&T("Object already scheduled for deletion");Ab(this);Bb(this.Na);this.Na.Nb||(this.Na.ab=void 0,this.Na.Sa=void 0)}; U.prototype.isDeleted=function(){return!this.Na.Sa};U.prototype.deleteLater=function(){this.Na.Sa||yb(this);this.Na.Cb&&!this.Na.Nb&&T("Object already scheduled for deletion");Eb.push(this);1===Eb.length&&Gb&&Gb(Fb);this.Na.Cb=!0;return this};e.getInheritedInstanceCount=function(){return Object.keys(Hb).length};e.getLiveInheritedInstances=function(){var a=[],b;for(b in Hb)Hb.hasOwnProperty(b)&&a.push(Hb[b]);return a};e.flushPendingDeletes=Fb;e.setDelayFunction=function(a){Gb=a;Eb.length&&Gb&&Gb(Fb)}; V.prototype.xd=function(a){this.Tc&&(a=this.Tc(a));return a};V.prototype.Hc=function(a){this.pb&&this.pb(a)};V.prototype.argPackAdvance=8;V.prototype.readValueFromPointer=lb;V.prototype.deleteObject=function(a){if(null!==a)a["delete"]()}; V.prototype.fromWireType=function(a){function b(){return this.Vb?Jb(this.Ta.Eb,{Xa:this.Md,Sa:c,fb:this,ab:a}):Jb(this.Ta.Eb,{Xa:this,Sa:a})}var c=this.xd(a);if(!c)return this.Hc(a),null;var d=Ib(this.Ta,c);if(void 0!==d){if(0===d.Na.count.value)return d.Na.Sa=c,d.Na.ab=a,d.clone();d=d.clone();this.Hc(a);return d}d=this.Ta.vd(c);d=Db[d];if(!d)return b.call(this);d=this.Tb?d.jd:d.pointerType;var f=Cb(c,this.Ta,d.Ta);return null===f?b.call(this):this.Vb?Jb(d.Ta.Eb,{Xa:d,Sa:f,fb:this,ab:a}):Jb(d.Ta.Eb, {Xa:d,Sa:f})};Wb=e.UnboundTypeError=rb("UnboundTypeError");e.count_emval_handles=function(){for(var a=0,b=5;bf?-28:N.Fc(d,f).fd;case 1:case 2:return 0;case 3:return d.flags;case 4:return f=hb(),d.flags|=f,0;case 5:return f=hb(),D[f+0>>1]=2,0;case 6:case 7:return 0;case 16:case 8:return-28;case 9:return E[Kc()>>2]=28,-1;default:return-28}}catch(g){if("undefined"== typeof N||!(g instanceof N.Ma))throw g;return-g.Ra}},X:function(a,b){try{var c=Q(a);return fb(N.stat,c.path,b)}catch(d){if("undefined"==typeof N||!(d instanceof N.Ma))throw d;return-d.Ra}},E:function(a,b,c){try{b=c+2097152>>>0<4194305-!!b?(b>>>0)+4294967296*c:NaN;if(isNaN(b))return-61;N.ud(a,b);return 0}catch(d){if("undefined"==typeof N||!(d instanceof N.Ma))throw d;return-d.Ra}},S:function(a,b){try{if(0===b)return-28;var c=N.cwd(),d=ta(c)+1;if(b>>0,(G=h,1<=+Math.abs(G)?0>>0:~~+Math.ceil((G-+(~~G>>>0))/4294967296)>>> 0:0)];E[b+a>>2]=J[0];E[b+a+4>>2]=J[1];J=[280*(g+1)>>>0,(G=280*(g+1),1<=+Math.abs(G)?0>>0:~~+Math.ceil((G-+(~~G>>>0))/4294967296)>>>0:0)];E[b+a+8>>2]=J[0];E[b+a+12>>2]=J[1];D[b+a+16>>1]=280;B[b+a+18>>0]=n;A(k,z,b+a+19,256);a+=280;g+=1}N.bb(d,280*g,0);return a}catch(p){if("undefined"==typeof N||!(p instanceof N.Ma))throw p;return-p.Ra}},Y:function(a,b,c){gb=c;try{var d=Q(a);switch(b){case 21509:case 21505:return d.tty?0:-59;case 21510:case 21511:case 21512:case 21506:case 21507:case 21508:return d.tty? 0:-59;case 21519:if(!d.tty)return-59;var f=hb();return E[f>>2]=0;case 21520:return d.tty?-28:-59;case 21531:return f=hb(),N.kc(d,b,f);case 21523:return d.tty?0:-59;case 21524:return d.tty?0:-59;default:v("bad ioctl syscall "+b)}}catch(g){if("undefined"==typeof N||!(g instanceof N.Ma))throw g;return-g.Ra}},V:function(a,b){try{return a=y(a),fb(N.lstat,a,b)}catch(c){if("undefined"==typeof N||!(c instanceof N.Ma))throw c;return-c.Ra}},P:function(a,b,c){try{return b=y(b),b=P(a,b),b=L(b),"/"===b[b.length- 1]&&(b=b.substr(0,b.length-1)),N.mkdir(b,c,0),0}catch(d){if("undefined"==typeof N||!(d instanceof N.Ma))throw d;return-d.Ra}},U:function(a,b,c,d){try{b=y(b);var f=d&256;b=P(a,b,d&4096);return fb(f?N.lstat:N.stat,b,c)}catch(g){if("undefined"==typeof N||!(g instanceof N.Ma))throw g;return-g.Ra}},v:function(a,b,c,d){gb=d;try{b=y(b);b=P(a,b);var f=d?hb():0;return N.open(b,c,f).fd}catch(g){if("undefined"==typeof N||!(g instanceof N.Ma))throw g;return-g.Ra}},L:function(a,b,c,d){try{b=y(b);b=P(a,b);if(0>= d)return-28;var f=N.readlink(b),g=Math.min(d,ta(f)),k=B[c+g];A(f,z,c,d+1);B[c+g]=k;return g}catch(h){if("undefined"==typeof N||!(h instanceof N.Ma))throw h;return-h.Ra}},K:function(a,b,c,d){try{return b=y(b),d=y(d),b=P(a,b),d=P(c,d),N.rename(b,d),0}catch(f){if("undefined"==typeof N||!(f instanceof N.Ma))throw f;return-f.Ra}},r:function(a){try{return a=y(a),N.rmdir(a),0}catch(b){if("undefined"==typeof N||!(b instanceof N.Ma))throw b;return-b.Ra}},W:function(a,b){try{return a=y(a),fb(N.stat,a,b)}catch(c){if("undefined"== typeof N||!(c instanceof N.Ma))throw c;return-c.Ra}},s:function(a,b,c){try{return b=y(b),b=P(a,b),0===c?N.unlink(b):512===c?N.rmdir(b):v("Invalid flags passed to unlinkat"),0}catch(d){if("undefined"==typeof N||!(d instanceof N.Ma))throw d;return-d.Ra}},I:function(a,b,c){try{b=y(b);b=P(a,b,!0);if(c){var d=ib(c),f=E[c+8>>2];g=1E3*d+f/1E6;c+=16;d=ib(c);f=E[c+8>>2];k=1E3*d+f/1E6}else var g=Date.now(),k=g;N.ae(b,g,k);return 0}catch(h){if("undefined"==typeof N||!(h instanceof N.Ma))throw h;return-h.Ra}}, f:function(a){var b=jb[a];delete jb[a];var c=b.rc,d=b.pb,f=b.Lc,g=f.map(k=>k.zd).concat(f.map(k=>k.Sd));ub([a],g,k=>{var h={};f.forEach((n,q)=>{var p=k[q],t=n.Sb,x=n.yd,l=k[q+f.length],u=n.Rd,w=n.Td;h[n.rd]={read:C=>p.fromWireType(t(x,C)),write:(C,K)=>{var H=[];u(w,C,l.toWireType(H,K));kb(H)}}});return[{name:b.name,fromWireType:function(n){var q={},p;for(p in h)q[p]=h[p].read(n);d(n);return q},toWireType:function(n,q){for(var p in h)if(!(p in q))throw new TypeError('Missing field: "'+p+'"');var t= c();for(p in h)h[p].write(t,q[p]);null!==n&&n.push(d,t);return t},argPackAdvance:8,readValueFromPointer:lb,ib:d}]})},F:function(){},fa:function(a,b,c,d,f){var g=vb(c);b=S(b);R(a,{name:b,fromWireType:function(k){return!!k},toWireType:function(k,h){return h?d:f},argPackAdvance:8,readValueFromPointer:function(k){if(1===c)var h=B;else if(2===c)h=D;else if(4===c)h=E;else throw new TypeError("Unknown boolean type size: "+b);return this.fromWireType(h[k>>g])},ib:null})},ka:function(a,b,c,d,f,g,k,h,n,q,p, t,x){p=S(p);g=W(f,g);h&&(h=W(k,h));q&&(q=W(n,q));x=W(t,x);var l=pb(p);Mb(l,function(){Zb("Cannot construct "+p+" due to unbound types",[d])});ub([a,b,c],d?[d]:[],function(u){u=u[0];if(d){var w=u.Ta;var C=w.Eb}else C=U.prototype;u=qb(l,function(){if(Object.getPrototypeOf(this)!==K)throw new xb("Use 'new' to construct "+p);if(void 0===H.tb)throw new xb(p+" has no accessible constructor");var I=H.tb[arguments.length];if(void 0===I)throw new xb("Tried to invoke ctor of "+p+" with invalid number of parameters ("+ arguments.length+") - expected ("+Object.keys(H.tb).toString()+") parameters instead!");return I.apply(this,arguments)});var K=Object.create(C,{constructor:{value:u}});u.prototype=K;var H=new Nb(p,u,K,x,w,g,h,q);w=new V(p,H,!0,!1);C=new V(p+"*",H,!1,!1);var Ga=new V(p+" const*",H,!1,!0);Db[a]={pointerType:C,jd:Ga};Ub(l,u);return[w,C,Ga]})},ja:function(a,b,c,d,f,g){0{Zb("Cannot construct "+h.name+" due to unbound types",k)};ub([],k,function(q){q.splice(1,0,null);h.Ta.tb[b-1]=bc(n,q,null,f,g);return[]});return[]})},b:function(a,b,c,d,f,g,k,h){var n=$b(c,d);b=S(b);g=W(f,g);ub([],[a],function(q){function p(){Zb("Cannot call "+ t+" due to unbound types",n)}q=q[0];var t=q.name+"."+b;b.startsWith("@@")&&(b=Symbol[b.substring(2)]);h&&q.Ta.Nd.push(b);var x=q.Ta.Eb,l=x[b];void 0===l||void 0===l.$a&&l.className!==q.name&&l.Qb===c-2?(p.Qb=c-2,p.className=q.name,x[b]=p):(Lb(x,b,t),x[b].$a[c-2]=p);ub([],n,function(u){u=bc(t,u,q,g,k);void 0===x[b].$a?(u.Qb=c-2,x[b]=u):x[b].$a[c-2]=u;return[]});return[]})},ea:function(a,b){b=S(b);R(a,{name:b,fromWireType:function(c){var d=ec(c);dc(c);return d},toWireType:function(c,d){return Sb(d)}, argPackAdvance:8,readValueFromPointer:lb,ib:null})},x:function(a,b,c){c=vb(c);b=S(b);R(a,{name:b,fromWireType:function(d){return d},toWireType:function(d,f){return f},argPackAdvance:8,readValueFromPointer:fc(b,c),ib:null})},ha:function(a,b,c,d,f,g){var k=$b(b,c);a=S(a);f=W(d,f);Mb(a,function(){Zb("Cannot call "+a+" due to unbound types",k)},b-1);ub([],k,function(h){h=[h[0],null].concat(h.slice(1));Ub(a,bc(a,h,null,f,g),b-1);return[]})},i:function(a,b,c,d,f){b=S(b);-1===f&&(f=4294967295);f=vb(c);var g= h=>h;if(0===d){var k=32-8*c;g=h=>h<>>k}c=b.includes("unsigned")?function(h,n){return n>>>0}:function(h,n){return n};R(a,{name:b,fromWireType:g,toWireType:c,argPackAdvance:8,readValueFromPointer:gc(b,f,0!==d),ib:null})},e:function(a,b,c){function d(g){g>>=2;var k=F;return new f(ua,k[g+1],k[g])}var f=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array][b];c=S(c);R(a,{name:c,fromWireType:d,argPackAdvance:8,readValueFromPointer:d},{Ad:!0})},y:function(a,b){b= S(b);var c="std::string"===b;R(a,{name:b,fromWireType:function(d){var f=F[d>>2],g=d+4;if(c)for(var k=g,h=0;h<=f;++h){var n=g+h;if(h==f||0==z[n]){k=y(k,n-k);if(void 0===q)var q=k;else q+=String.fromCharCode(0),q+=k;k=n+1}}else{q=Array(f);for(h=0;h>2]=k;if(c&&g)A(f,z,n,k+1);else if(g)for(g=0;gva;var h=1}else 4===b&&(d=lc,f=mc,g=nc,k=()=>F,h=2);R(a,{name:c,fromWireType:function(n){for(var q= F[n>>2],p=k(),t,x=n+4,l=0;l<=q;++l){var u=n+4+l*b;if(l==q||0==p[u>>h])x=d(x,u-x),void 0===t?t=x:(t+=String.fromCharCode(0),t+=x),x=u+b}X(n);return t},toWireType:function(n,q){"string"!=typeof q&&T("Cannot pass non-string to C++ string type "+c);var p=g(q),t=wc(4+p+b);F[t>>2]=p>>h;f(q,t+4,p+b);null!==n&&n.push(X,t);return t},argPackAdvance:8,readValueFromPointer:lb,ib:function(n){X(n)}})},g:function(a,b,c,d,f,g){jb[a]={name:S(b),rc:W(c,d),pb:W(f,g),Lc:[]}},d:function(a,b,c,d,f,g,k,h,n,q){jb[a].Lc.push({rd:S(b), -zd:c,Sb:W(d,f),yd:g,Sd:k,Rd:W(h,n),Td:q})},ga:function(a,b){b=S(b);R(a,{Dd:!0,name:b,argPackAdvance:0,fromWireType:function(){},toWireType:function(){}})},o:function(){return Date.now()},aa:function(){return!0},m:function(a,b,c){a=ec(a);b=oc(b,"emval::as");var d=[],f=Sb(d);F[c>>2]=f;return b.toWireType(d,a)},A:function(a,b,c,d){a=rc[a];b=ec(b);c=qc(c);a(b,c,null,d)},na:dc,oa:function(a,b){var c=tc(a,b),d=c[0];b=d.name+"_$"+c.slice(1).map(function(p){return p.name}).join("_")+"$";var f=uc[b];if(void 0!== +zd:c,Sb:W(d,f),yd:g,Sd:k,Rd:W(h,n),Td:q})},ga:function(a,b){b=S(b);R(a,{Dd:!0,name:b,argPackAdvance:0,fromWireType:function(){},toWireType:function(){}})},o:function(){return Date.now()},aa:function(){return!0},l:function(a,b,c){a=ec(a);b=oc(b,"emval::as");var d=[],f=Sb(d);F[c>>2]=f;return b.toWireType(d,a)},A:function(a,b,c,d){a=rc[a];b=ec(b);c=qc(c);a(b,c,null,d)},na:dc,oa:function(a,b){var c=tc(a,b),d=c[0];b=d.name+"_$"+c.slice(1).map(function(p){return p.name}).join("_")+"$";var f=uc[b];if(void 0!== f)return f;f=["retType"];for(var g=[d],k="",h=0;h>2]=a.getSeconds();E[b+4>>2]=a.getMinutes();E[b+8>>2]=a.getHours();E[b+12>>2]=a.getDate();E[b+16>>2]=a.getMonth();E[b+20>>2]=a.getFullYear()- 1900;E[b+24>>2]=a.getDay();var c=new Date(a.getFullYear(),0,1);E[b+28>>2]=(a.getTime()-c.getTime())/864E5|0;E[b+36>>2]=-(60*a.getTimezoneOffset());var d=(new Date(a.getFullYear(),6,1)).getTimezoneOffset();c=c.getTimezoneOffset();E[b+32>>2]=(d!=c&&a.getTimezoneOffset()==Math.min(c,d))|0},N:function(a,b,c,d,f,g){try{var k=N.rb(d);if(!k)return-8;var h=N.yb(k,a,f,b,c),n=h.Sa;E[g>>2]=h.zc;return n}catch(q){if("undefined"==typeof N||!(q instanceof N.Ma))throw q;return-q.Ra}},O:function(a,b,c,d,f,g){try{var k= -N.rb(f);if(k&&c&2){var h=z.slice(a,a+b);N.Fb(k,h,g,b,d)}}catch(n){if("undefined"==typeof N||!(n instanceof N.Ma))throw n;return-n.Ra}},ca:yc,l:function(){v("")},J:function(){return 2147483648},w:zc,n:function(a){var b=z.length;a>>>=0;if(2147483648=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);var f=Math;d=Math.max(a,d);f=f.min.call(f,2147483648,d+(65536-d%65536)%65536);a:{try{pa.grow(f-ua.byteLength+65535>>>16);ya();var g=1;break a}catch(k){}g=void 0}if(g)return!0}return!1}, +N.rb(f);if(k&&c&2){var h=z.slice(a,a+b);N.Fb(k,h,g,b,d)}}catch(n){if("undefined"==typeof N||!(n instanceof N.Ma))throw n;return-n.Ra}},ca:yc,m:function(){v("")},J:function(){return 2147483648},w:zc,n:function(a){var b=z.length;a>>>=0;if(2147483648=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);var f=Math;d=Math.max(a,d);f=f.min.call(f,2147483648,d+(65536-d%65536)%65536);a:{try{pa.grow(f-ua.byteLength+65535>>>16);ya();var g=1;break a}catch(k){}g=void 0}if(g)return!0}return!1}, Q:function(a,b){var c=0;Bc().forEach(function(d,f){var g=b+c;f=F[a+4*f>>2]=g;for(g=0;g>0]=d.charCodeAt(g);B[f>>0]=0;c+=d.length+1});return 0},R:function(a,b){var c=Bc();F[a>>2]=c.length;var d=0;c.forEach(function(f){d+=f.length+1});F[b>>2]=d;return 0},k:function(a){try{var b=Q(a);N.close(b);return 0}catch(c){if("undefined"==typeof N||!(c instanceof N.Ma))throw c;return c.Ra}},H:function(a,b){try{var c=Q(a);B[b>>0]=c.tty?2:N.Ya(c.mode)?3:N.vb(c.mode)?7:4;return 0}catch(d){if("undefined"== typeof N||!(d instanceof N.Ma))throw d;return d.Ra}},u:function(a,b,c,d){try{a:{var f=Q(a);a=b;for(var g=b=0;g>2],h=F[a+4>>2];a+=8;var n=N.read(f,B,k,h,void 0);if(0>n){var q=-1;break a}b+=n;if(n>2]=q;return 0}catch(p){if("undefined"==typeof N||!(p instanceof N.Ma))throw p;return p.Ra}},D:function(a,b,c,d,f){try{b=c+2097152>>>0<4194305-!!b?(b>>>0)+4294967296*c:NaN;if(isNaN(b))return 61;var g=Q(a);N.bb(g,b,d);J=[g.position>>>0,(G=g.position,1<=+Math.abs(G)?0>>0:~~+Math.ceil((G-+(~~G>>>0))/4294967296)>>>0:0)];E[f>>2]=J[0];E[f+4>>2]=J[1];g.ub&&0===b&&0===d&&(g.ub=null);return 0}catch(k){if("undefined"==typeof N||!(k instanceof N.Ma))throw k;return k.Ra}},T:function(a){try{var b=Q(a);return b.Qa&&b.Qa.fsync?-b.Qa.fsync(b):0}catch(c){if("undefined"==typeof N||!(c instanceof N.Ma))throw c;return c.Ra}},t:function(a,b,c,d){try{a:{var f=Q(a);a=b;for(var g=b=0;g>2],h=F[a+4>>2];a+=8;var n=N.write(f,B,k,h,void 0);if(0> n){var q=-1;break a}b+=n}q=b}F[d>>2]=q;return 0}catch(p){if("undefined"==typeof N||!(p instanceof N.Ma))throw p;return p.Ra}},q:function(){},ia:Gc,G:function(a,b,c,d){return Gc(a,b,c,d)}},Z=function(){function a(c){e.asm=c.exports;pa=e.asm.pa;ya();za=e.asm.ua;Ba.unshift(e.asm.qa);Ja("wasm-instantiate")}var b={a:Lc};Ia("wasm-instantiate");if(e.instantiateWasm)try{return e.instantiateWasm(b,a)}catch(c){return r("Module.instantiateWasm callback failed with error: "+c),!1}b=Ma(b);a(b[0]);return e.asm}(); e.___wasm_call_ctors=Z.qa;var Kc=e.___errno_location=Z.ra,wc=e._malloc=Z.sa,X=e._free=Z.ta,Yb=e.___getTypeName=Z.va;e.___embind_register_native_and_builtin_types=Z.wa;var cb=e._emscripten_builtin_memalign=Z.xa;e.___cxa_is_pointer_type=Z.ya;e.dynCall_iiiij=Z.za;e.dynCall_iij=Z.Aa;e.dynCall_iijii=Z.Ba;e.dynCall_iiji=Z.Ca;e.dynCall_iiiiiij=Z.Da;e.dynCall_vjii=Z.Ea;e.dynCall_vji=Z.Fa;e.dynCall_ji=Z.Ga;e.dynCall_jiji=Z.Ha;e.dynCall_viijii=Z.Ia;e.dynCall_iiiiij=Z.Ja;e.dynCall_iiiiijj=Z.Ka; e.dynCall_iiiiiijj=Z.La;e.addRunDependency=Ia;e.removeRunDependency=Ja;e.FS_createPath=N.Dc;e.FS_createDataFile=N.Rb;e.FS_createPreloadedFile=N.Ec;e.FS_createLazyFile=N.Cc;e.FS_createDevice=N.hb;e.FS_unlink=N.unlink;e.FS=N;var Mc;Ha=function Nc(){Mc||Oc();Mc||(Ha=Nc)}; function Oc(){function a(){if(!Mc&&(Mc=!0,e.calledRun=!0,!qa)){e.noFSInit||N.Db.jc||N.Db();N.Rc=!1;Na(Ba);aa(e);if(e.onRuntimeInitialized)e.onRuntimeInitialized();if(e.postRun)for("function"==typeof e.postRun&&(e.postRun=[e.postRun]);e.postRun.length;){var b=e.postRun.shift();Ca.unshift(b)}Na(Ca)}}if(!(0): void; getAllMessagesWeb(): $ReadOnlyArray<{ +message: WebMessage, +medias: $ReadOnlyArray, }>; removeAllMessages(): void; removeMessages(ids: $ReadOnlyArray): void; removeMessagesForThreads(threadIDs: $ReadOnlyArray): void; replaceMessageWeb(message: WebMessage): void; rekeyMessage(from: string, to: string): void; removeAllMedia(): void; removeMediaForThreads(threadIDs: $ReadOnlyArray): void; removeMediaForMessages(msgIDs: $ReadOnlyArray): void; removeMediaForMessage(msgID: string): void; replaceMedia(media: Media): void; rekeyMediaContainers(from: string, to: string): void; replaceMessageStoreThreads( threads: $ReadOnlyArray<{ +id: string, +startReached: number }>, ): void; removeMessageStoreThreads($ReadOnlyArray): void; getAllMessageStoreThreads(): $ReadOnlyArray<{ +id: string, +startReached: number, }>; removeAllMessageStoreThreads(): void; setMetadata(entryName: string, data: string): void; clearMetadata(entryName: string): void; getMetadata(entryName: string): string; replaceReport(report: ClientDBReport): void; removeReports(ids: $ReadOnlyArray): void; removeAllReports(): void; getAllReports(): ClientDBReport[]; setPersistStorageItem(key: string, item: string): void; removePersistStorageItem(key: string): void; getPersistStorageItem(key: string): string; replaceUser(userInfo: ClientDBUserInfo): void; removeUsers(ids: $ReadOnlyArray): void; removeAllUsers(): void; getAllUsers(): ClientDBUserInfo[]; replaceThreadWeb(thread: WebClientDBThreadInfo): void; removeThreads(ids: $ReadOnlyArray): void; removeAllThreads(): void; getAllThreadsWeb(): WebClientDBThreadInfo[]; replaceKeyserver(user_info: ClientDBKeyserverInfo): void; removeKeyservers(ids: $ReadOnlyArray): void; removeAllKeyservers(): void; getAllKeyservers(): ClientDBKeyserverInfo[]; beginTransaction(): void; commitTransaction(): void; rollbackTransaction(): void; getOlmPersistAccountDataWeb(): NullableString; getOlmPersistSessionsData(): $ReadOnlyArray; storeOlmPersistAccount(accountData: string): void; storeOlmPersistSession(session: OlmPersistSession): void; restoreFromMainCompaction( mainCompactionPath: string, mainCompactionEncryptionKey: string, ): void; + restoreFromBackupLog(backupLog: Uint8Array): void; + // method is provided to manually signal that a C++ object // is no longer needed and can be deleted delete(): void; } export type SQLiteQueryExecutorType = typeof SQLiteQueryExecutor; diff --git a/web/scripts/run_emscripten.sh b/web/scripts/run_emscripten.sh index 461971b60..ac375adfc 100755 --- a/web/scripts/run_emscripten.sh +++ b/web/scripts/run_emscripten.sh @@ -1,169 +1,171 @@ #!/usr/bin/env bash set -Eeuo pipefail if ! command -v emcc > /dev/null; then echo "Please install emscripten or run 'nix develop'" >&2 exit 1 fi # directories SCRIPT_DIR=$(cd "$(dirname "$0")"; pwd -P) NATIVE_CPP_DIR="${SCRIPT_DIR}/../../native/cpp/" INPUT_DIR="${NATIVE_CPP_DIR}CommonCpp/DatabaseManagers/" ENTITIES_DIR="${NATIVE_CPP_DIR}CommonCpp/DatabaseManagers/entities/" SQLITE_DIR="${SCRIPT_DIR}/../database/sqlite/" WEB_CPP_DIR="${SCRIPT_DIR}/../cpp/" OUTPUT_DIR="${SCRIPT_DIR}/../database/_generated/" # files SQLITE_SOURCE="${SQLITE_DIR}sqlite3.c" SQLITE_BITCODE_FILE="${SQLITE_DIR}sqlite3.bc" OUTPUT_FILE_NAME="comm-query-executor" OUTPUT_FILE="${OUTPUT_DIR}${OUTPUT_FILE_NAME}.js" # OpenSSL resources OPENSSL_VERSION="3.2.0" OPENSSL_URL="https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz" OPENSSL_FILE="${SQLITE_DIR}openssl-file" OPENSSL_DIR="${SQLITE_DIR}openssl-${OPENSSL_VERSION}" OPENSSL_HEADERS="${OPENSSL_DIR}/include" OPENSSL_LIBCRYPTO="${OPENSSL_DIR}/libcrypto.a" # SQLCipher resources -SQLCIPHER_AMALGAMATION_VERSION="4.5.5b" +SQLCIPHER_AMALGAMATION_VERSION="4.5.5-d" SQLCIPHER_AMALGAMATION="sqlcipher-amalgamation-${SQLCIPHER_AMALGAMATION_VERSION}" SQLCIPHER_AMALGAMATION_URL="https://codeload.github.com/CommE2E/sqlcipher-amalgamation/zip/refs/tags/${SQLCIPHER_AMALGAMATION_VERSION}" SQLCIPHER_AMALGAMATION_FILE="${SQLITE_DIR}${SQLCIPHER_AMALGAMATION}" SQLITE_COMPILATION_FLAGS=( -Oz -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_DISABLE_LFS -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_THREADSAFE=0 -DSQLITE_ENABLE_NORMALIZE -DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=2 -DSQLCIPHER_CRYPTO_OPENSSL + -DSQLITE_ENABLE_SESSION + -DSQLITE_ENABLE_PREUPDATE_HOOK ) download_openssl() { mkdir -p "$SQLITE_DIR" curl "${OPENSSL_URL}" --output "${OPENSSL_FILE}" tar -xf "${OPENSSL_FILE}" -C "${SQLITE_DIR}" rm -f "${OPENSSL_FILE}" } build_openssl() { pushd "${OPENSSL_DIR}" ./Configure \ no-asm \ no-async \ no-egd \ no-ktls \ no-module \ no-posix-io \ no-secure-memory \ no-dso \ no-shared \ no-sock \ no-stdio \ no-ui-console \ no-weak-ssl-ciphers \ no-engine \ linux-generic32 make CC="emcc" AR="emar" RANLIB="emranlib" popd } if [ ! -d "$OPENSSL_DIR" ]; then echo "OpenSSL sources not found. Downloading." download_openssl fi if [ ! -f "$OPENSSL_LIBCRYPTO" ]; then echo "OpenSSL binary not found. Building." build_openssl fi download_sqlite() { mkdir -p "$SQLITE_DIR" curl "${SQLCIPHER_AMALGAMATION_URL}" --output "${SQLCIPHER_AMALGAMATION_FILE}" unzip -jo "${SQLCIPHER_AMALGAMATION_FILE}" -d "${SQLITE_DIR}" rm -f "${SQLCIPHER_AMALGAMATION_FILE}" } if [ ! -f "$SQLITE_BITCODE_FILE" ]; then echo "SQLite engine not found. Downloading." download_sqlite emcc "${SQLITE_COMPILATION_FLAGS[@]}" \ -I "${OPENSSL_HEADERS}" \ -c "$SQLITE_SOURCE" \ -o "$SQLITE_BITCODE_FILE" fi EMCC_FLAGS=( # WASM files and bindings --memory-init-file 0 -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s ALLOW_TABLE_GROWTH=1 -s FORCE_FILESYSTEM=1 -s SINGLE_FILE=0 -s EXPORTED_RUNTIME_METHODS=["FS"] # node/babel/webpack helpers -s NODEJS_CATCH_EXIT=0 -s NODEJS_CATCH_REJECTION=0 -s WASM_ASYNC_COMPILATION=0 -s EXPORT_ES6=1 -s USE_ES6_IMPORT_META=0 -s MODULARIZE=1 # optimization -Oz -flto --closure 1 ) CFLAGS=( -I "$INPUT_DIR" -I "$SQLITE_DIR" -I "${NATIVE_CPP_DIR}CommonCpp/Tools/" ) INPUT_FILES=( "${INPUT_DIR}SQLiteConnectionManager.cpp" "${WEB_CPP_DIR}SQLiteQueryExecutorBindings.cpp" "${WEB_CPP_DIR}Logger.cpp" "${ENTITIES_DIR}SQLiteDataConverters.cpp" "${ENTITIES_DIR}SQLiteStatementWrapper.cpp" "$SQLITE_BITCODE_FILE" ) mkdir -p "$OUTPUT_DIR" emcc -lembind \ "${EMCC_FLAGS[@]}" \ "${CFLAGS[@]}" \ "${INPUT_FILES[@]}" \ "${OPENSSL_LIBCRYPTO}" \ -o "${OUTPUT_FILE}" \ -std=c++17 GENERATED_TAG="generated" sed -i.bak -e "1i\/\/ \@${GENERATED_TAG}" "${OUTPUT_FILE}" mv -f "${OUTPUT_DIR}${OUTPUT_FILE_NAME}.wasm" "${OUTPUT_DIR}comm_query_executor.wasm" rm -f "${OUTPUT_FILE}.bak"