diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index c0abb99ca994d4d9ff91947fe00f5dbf0dbeaeee..8a572172298812028952bbdab096defa03015dbc 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -60,6 +60,7 @@ set(VEREIGNLIB_SRC vereign/grpc/service_registry.cc vereign/grpc/server.cc + vereign/sqlite/statement.cc vereign/sqlite/connection.cc vereign/bytes/view_dump.cc @@ -75,6 +76,7 @@ set(VEREIGNLIB_SRC vereign/crypto/digest.cc + vereign/kvstore/lock.cc vereign/kvstore/sqlite_storage.cc vereign/kvstore/crypto_storage.cc diff --git a/cpp/src/csandbox.cc b/cpp/src/csandbox.cc index e8288a68446d589959df01778774b5344b9429fa..cdf8de983f263a51b356e9ea05961554cb6db480 100644 --- a/cpp/src/csandbox.cc +++ b/cpp/src/csandbox.cc @@ -12,39 +12,5 @@ auto main(int argc, char** argv) -> int { boost::ignore_unused(argc); boost::ignore_unused(argv); - // fs::path ; - - // std::wstring s = L"c:\\проба.txt"; - std::string path = "c:\\ztrash\\проба.txt"; - std::wstring ws = vereign::string::widen(path); - - std::string p2 = vereign::string::narrow(ws); - std::cout << vereign::bytes::dump(vereign::bytes::View(path)) << std::endl; - std::cout << vereign::bytes::dump(vereign::bytes::View(p2)) << std::endl; - if (p2 == path) { - std::cout << "yes" << std::endl; - } - - std::cout << vereign::bytes::dump(vereign::bytes::View(ws)) << std::endl; - - // std::ofstream f{ws}; - // std::wcout << ws; - std::cout << path << std::endl; - std::wcout << ws << std::endl; - // std::cout << "проба" << std::endl; - // wchar_t ru[] = L"Привет"; //Russian language - // std::wcout << ru; - // std::cout << std::endl; - // std::cout << "Temp directory is " << fs::temp_directory_path() << '\n'; - // std::cout << "Home directory is " << fs::user << '\n'; - - // p.wstring(); - // std::cout << p.string() << std::endl;; - - // wchar_t* in = L"ทดสà¸à¸š"; // thai language - // char* out=(char *)malloc(15); - // WideCharToMultiByte(874, 0, in, 15, out, 15, NULL, NULL); - // printf(out); // result is correctly in Thai although not neat - - return 1; + return 0; } diff --git a/cpp/src/vereign/bytes/view.hh b/cpp/src/vereign/bytes/view.hh index c64f59a14003857e8d7b5c3f7efbf0b619ad8f97..672b6229ada16d3b2b1a1351ddcc3865d30a2e3e 100644 --- a/cpp/src/vereign/bytes/view.hh +++ b/cpp/src/vereign/bytes/view.hh @@ -80,6 +80,10 @@ public: return std::memcmp(data_, other.data_, size_) == 0; } + auto operator!=(View other) const noexcept -> bool { + return !(*this == other); + } + auto operator[](std::size_t index) const -> const uint8_t& { if (index >= size_ ) { throw std::runtime_error("index out of bounds"); diff --git a/cpp/src/vereign/grpc/server.hh b/cpp/src/vereign/grpc/server.hh index 5ab7301ba1e614ad8f340948ab67d5508b613231..9b992d5a0974c8d48f9d150c867078052bd1b462 100644 --- a/cpp/src/vereign/grpc/server.hh +++ b/cpp/src/vereign/grpc/server.hh @@ -9,7 +9,7 @@ namespace vereign::grpc { /** * BindError is thrown when the Server::Server could not start listening. */ -class BindError: public virtual std::exception { +class BindError: public std::exception { public: auto what() const noexcept -> const char* override { return "gRPC listen failed"; diff --git a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc index 66ecd53e8e80c36fca05871db698fcbda057bc4e..b70efafb860153bc30415a7844f60a8a68b58a07 100644 --- a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc +++ b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc @@ -2,24 +2,25 @@ #include <vereign/kvstore/detail/value_encoder.hh> #include <vereign/kvstore/errors.hh> +#include <vereign/kvstore/lock.hh> +#include <vereign/crypto/digest.hh> +#include <vereign/crypto/errors.hh> #include <vereign/crypto/rand.hh> #include <vereign/crypto/rsa.hh> #include <vereign/crypto/aes.hh> #include <vereign/crypto/bio.hh> -#include <openssl/base.h> -#include <openssl/bn.h> -#include <openssl/evp.h> -#include <openssl/sha.h> -#include <openssl/aes.h> -#include <openssl/rsa.h> -#include <openssl/pem.h> +#include <chrono> namespace { + // FIXME: should these be injected and provided by the integrator constexpr int iterations = 1 << 18; constexpr int saltSizeBytes = 64; constexpr int aesKeySizeBytes = 32; + + constexpr int lockRetryCount = 10; + constexpr auto lockRetrySleep = std::chrono::milliseconds{1000}; } namespace vereign::kvstore::detail { @@ -29,12 +30,14 @@ CryptoStorageImpl::CryptoStorageImpl(kvstore::Storage& storage) {} void CryptoStorageImpl::Open(const std::string& pin) { + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + bytes::Buffer salt; - auto iterations = storage_.GetInt64("master_key_iterations"); + auto iterations = storage_.GetInt64("__master_key_iterations"); if (iterations == 0) { throw StorageNotInitializedError{}; } - storage_.GetBytes("master_key_salt", salt); + storage_.GetBytes("__master_key_salt", salt); bytes::Buffer key{aesKeySizeBytes}; @@ -54,6 +57,16 @@ void CryptoStorageImpl::Open(const std::string& pin) { key.IncSize(aesKeySizeBytes); key_ = std::move(key); + + // FIXME: write tests for tampering and regular identity change + try { + bytes::Buffer tag; + GetBytes("__tag", tag); + tag_ = std::move(tag); + + } catch (const crypto::Error& err) { + throw IdentityChanged{}; + } } void CryptoStorageImpl::Reset(const std::string& pin) { @@ -76,14 +89,25 @@ void CryptoStorageImpl::Reset(const std::string& pin) { } key.IncSize(aesKeySizeBytes); + key_ = std::move(key); - storage_.PutInt64("master_key_iterations", iterations); - storage_.PutBytes("master_key_salt", salt.View()); + bytes::Buffer tag{saltSizeBytes}; + crypto::Rand(tag); - key_ = std::move(key); + { + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + + storage_.DeleteAll(); + + encryptBytes("__tag", tag.View()); + tag_ = std::move(tag); + + storage_.PutInt64("__master_key_iterations", iterations); + storage_.PutBytes("__master_key_salt", salt.View()); + } } -void CryptoStorageImpl::PutBytes(const std::string& key, bytes::View value) { +void CryptoStorageImpl::encryptBytes(const std::string& key, bytes::View value) { if (key_.Size() == 0) { throw Error("crypto storage is not initialized"); } @@ -100,7 +124,26 @@ void CryptoStorageImpl::PutBytes(const std::string& key, bytes::View value) { storage_.PutBytes(key, encoded_value.View()); } +void CryptoStorageImpl::PutBytes(const std::string& key, bytes::View value) { + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + + bytes::Buffer tag; + try { + GetBytes("__tag", tag); + } catch (const crypto::Error& err) { + throw IdentityChanged{}; + } + + if (tag.View() != tag_.View()) { + throw IdentityChanged{}; + } + + encryptBytes(key, value); +} + void CryptoStorageImpl::GetBytes(const std::string& key, bytes::Buffer& value) { + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + if (key_.Size() == 0) { throw Error("crypto storage is not initialized"); } diff --git a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh index 1a0cdc926400d2a3f235afa3180a4ecacce91064..327603621fb64ecbd9b05ccd3740ca3e5287933a 100644 --- a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh +++ b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh @@ -19,10 +19,15 @@ public: void PutBytes(const std::string& key, bytes::View value); void GetBytes(const std::string& key, bytes::Buffer& value); +private: + void encryptBytes(const std::string& key, bytes::View value); + private: kvstore::Storage& storage_; bytes::Buffer key_; + + bytes::Buffer tag_; }; } // namespace vereign::kvstore::detail diff --git a/cpp/src/vereign/kvstore/errors.hh b/cpp/src/vereign/kvstore/errors.hh index 694b3c5d38c0a4b91c134ed56ae9b397d0c17a49..371f273b1257ba5a464f2d03737a8bb86536707e 100644 --- a/cpp/src/vereign/kvstore/errors.hh +++ b/cpp/src/vereign/kvstore/errors.hh @@ -21,6 +21,22 @@ public: } }; +class LockError : public Error { +public: + LockError() + : Error{"cannot acquire storage lock"} + { + } +}; + +class IdentityChanged : public Error { +public: + IdentityChanged() + : Error{"identity has been changed"} + { + } +}; + } // namespace vereign::kvstore #endif // __VEREIGN_KVSTORE_ERRORS_HH diff --git a/cpp/src/vereign/kvstore/lock.cc b/cpp/src/vereign/kvstore/lock.cc new file mode 100644 index 0000000000000000000000000000000000000000..65935c0c3583dfcba3cb52bd11b2c204d4f48056 --- /dev/null +++ b/cpp/src/vereign/kvstore/lock.cc @@ -0,0 +1,39 @@ +#include <vereign/kvstore/lock.hh> + +#include <vereign/kvstore/errors.hh> + +#include <thread> +#include <sqlite3.h> + +namespace vereign::kvstore { + +Lock::Lock(Storage& storage) + : storage_{storage} +{ + storage.Lock(); +} + +Lock::Lock(Storage& storage, int retry_count, std::chrono::milliseconds sleep_interval) + : storage_{storage} +{ + for (int i = 0; i < retry_count; i++) { + try { + storage.Lock(); + + return; + } catch (const LockError& err) { + std::this_thread::sleep_for(sleep_interval); + + continue; + } + } + + throw LockError{}; +} + +Lock::~Lock() noexcept { + storage_.Unlock(); +} + + +} // namespace vereign::kvstore diff --git a/cpp/src/vereign/kvstore/lock.hh b/cpp/src/vereign/kvstore/lock.hh new file mode 100644 index 0000000000000000000000000000000000000000..38d0d47da88f32e4d117c23530712080456192a6 --- /dev/null +++ b/cpp/src/vereign/kvstore/lock.hh @@ -0,0 +1,25 @@ +#ifndef __VEREIGN_KVSTORE_LOCK_HH +#define __VEREIGN_KVSTORE_LOCK_HH + +#include <vereign/kvstore/storage.hh> +#include <chrono> + +namespace vereign::kvstore { + +class Lock { +public: + explicit Lock(Storage& storage); + Lock(Storage& storage, int retry_count, std::chrono::milliseconds sleep_interval); + ~Lock() noexcept; + + Lock(const Lock&) = delete; + auto operator=(const Lock&) -> Lock& = delete; + +private: + Storage& storage_; +}; + + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_LOCK_HH diff --git a/cpp/src/vereign/kvstore/sqlite_storage.cc b/cpp/src/vereign/kvstore/sqlite_storage.cc index de5b48d28b5f2e68628f4c1b3b155de3f29f4db9..3e45d0f5f6bd74806b7ad2a1a5f49d9c9e765a5e 100644 --- a/cpp/src/vereign/kvstore/sqlite_storage.cc +++ b/cpp/src/vereign/kvstore/sqlite_storage.cc @@ -1,80 +1,81 @@ #include <vereign/kvstore/sqlite_storage.hh> -#include <vereign/encoding/binary.hh> #include <vereign/kvstore/errors.hh> +#include <vereign/kvstore/lock.hh> + +#include <vereign/encoding/binary.hh> #include <boost/optional.hpp> #include <vereign/core/lock_guard.hh> +#include <vereign/sqlite/errors.hh> +#include <sqlite3.h> -#include <mutex> #include <array> +namespace { +constexpr int createTableRetryCount = 10; +constexpr auto createTableRetrySleep = std::chrono::milliseconds{1000}; +} + namespace vereign::kvstore { -namespace detail { +SqliteStorage::SqliteStorage(const std::string& db_path) + : db_{db_path} +{ + kvstore::Lock l{*this, createTableRetryCount, createTableRetrySleep}; -class SqliteLock { -public: - SqliteLock(sqlite::Connection& db) - : db_{db} - { - } + db_.Execute(R"( +CREATE TABLE IF NOT EXISTS storage ( + key TEXT PRIMARY KEY NOT NULL, + value BLOB +); + )"); +} - ~SqliteLock() { - Unlock(); +SqliteStorage::~SqliteStorage() { + if (lock_count_ != 0) { + db_.Commit(); } +} - void Lock() { - if (!tr_) { - tr_ = db_.BeginExplicitTransaction(); - } +void SqliteStorage::Lock() { + if (lock_count_ != 0) { + lock_count_++; - count_++; + return; } - void Unlock() { - if (count_ == 0 || !tr_) { - throw Error{"SqliteStorage unexpected call Unlock with non existent lock"}; + try { + db_.BeginExplicitTransaction(); + lock_count_++; + } catch (const sqlite::Error& err) { + if (err.code() == SQLITE_BUSY) { + throw LockError{}; } - count_--; - if (count_ == 0) { - tr_->Commit(); - tr_.reset(); - } + throw; } - -private: - int count_ = 0; - boost::optional<sqlite::Transaction> tr_; - sqlite::Connection& db_; -}; - } -SqliteStorage::SqliteStorage(const std::string& db_path) - : db_{db_path}, - lock_{std::make_unique<detail::SqliteLock>(db_)} -{ - db_.Execute(R"( -CREATE TABLE IF NOT EXISTS storage ( - key TEXT PRIMARY KEY NOT NULL, - value BLOB -); - )"); +void SqliteStorage::Unlock() { + if (lock_count_ == 0) { + throw Error{"unexpected call Unlock with non existent lock"}; + } + + lock_count_--; + if (lock_count_ == 0) { + db_.Commit(); + } } -SqliteStorage::~SqliteStorage() = default; -void SqliteStorage::Lock() { - lock_->Lock(); -} +void SqliteStorage::DeleteAll() { + kvstore::Lock l{*this}; -void SqliteStorage::Unlock() { - lock_->Unlock(); + db_.Execute("DELETE FROM storage;"); } void SqliteStorage::PutBytes(const std::string& key, bytes::View value) { - core::LockGuard<detail::SqliteLock> l{*lock_}; + kvstore::Lock l{*this}; auto stmt = db_.Prepare("REPLACE INTO storage(key, value) VALUES(?, ?);"); stmt.BindText(1, key); @@ -83,7 +84,7 @@ void SqliteStorage::PutBytes(const std::string& key, bytes::View value) { } void SqliteStorage::GetBytes(const std::string& key, bytes::Buffer& value) { - core::LockGuard<detail::SqliteLock> l{*lock_}; + kvstore::Lock l{*this}; auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?"); stmt.BindText(1, key); @@ -95,7 +96,7 @@ void SqliteStorage::GetBytes(const std::string& key, bytes::Buffer& value) { } void SqliteStorage::PutInt64(const std::string& key, int64_t value) { - core::LockGuard<detail::SqliteLock> l{*lock_}; + kvstore::Lock l{*this}; std::array<uint8_t, 8> encoded; encoding::binary::EncodeUint64(encoded.data(), value); @@ -107,7 +108,7 @@ void SqliteStorage::PutInt64(const std::string& key, int64_t value) { } auto SqliteStorage::GetInt64(const std::string& key) -> int64_t { - core::LockGuard<detail::SqliteLock> l{*lock_}; + kvstore::Lock l{*this}; auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?"); stmt.BindText(1, key); diff --git a/cpp/src/vereign/kvstore/sqlite_storage.hh b/cpp/src/vereign/kvstore/sqlite_storage.hh index 012c35f494faae64d4bfc04b9e23cfb19b6d8555..b07f39f49921b46d4fb7620f8624fac51ffcc5ef 100644 --- a/cpp/src/vereign/kvstore/sqlite_storage.hh +++ b/cpp/src/vereign/kvstore/sqlite_storage.hh @@ -4,14 +4,8 @@ #include <vereign/kvstore/storage.hh> #include <vereign/sqlite/connection.hh> -#include <memory> - namespace vereign::kvstore { -namespace detail { -class SqliteLock; -} - class SqliteStorage : public Storage { public: SqliteStorage(const std::string& db_path); @@ -23,6 +17,8 @@ public: void Lock() override; void Unlock() override; + void DeleteAll() override; + void PutBytes(const std::string& key, bytes::View value) override; void GetBytes(const std::string& key, bytes::Buffer& value) override; @@ -31,7 +27,7 @@ public: private: sqlite::Connection db_; - std::unique_ptr<detail::SqliteLock> lock_; + int lock_count_ = 0; }; } // namespace vereign::kvstore diff --git a/cpp/src/vereign/kvstore/storage.hh b/cpp/src/vereign/kvstore/storage.hh index 2bdb268d6395ceb981bed9adf6b442e7b34c0fe1..aa21c54684d0aef25313459f9b0f0194847beff1 100644 --- a/cpp/src/vereign/kvstore/storage.hh +++ b/cpp/src/vereign/kvstore/storage.hh @@ -10,6 +10,8 @@ public: virtual void Lock() = 0; virtual void Unlock() = 0; + virtual void DeleteAll() = 0; + virtual void PutBytes(const std::string& key, bytes::View value) = 0; virtual void GetBytes(const std::string& key, bytes::Buffer& value) = 0; diff --git a/cpp/src/vereign/sqlite/connection.cc b/cpp/src/vereign/sqlite/connection.cc index e12fb7c4a85e97df396c12c2d8649290e05b1499..eefe6a7de2ff004c777f88deb81eb3cee64f9271 100644 --- a/cpp/src/vereign/sqlite/connection.cc +++ b/cpp/src/vereign/sqlite/connection.cc @@ -1,163 +1,56 @@ -#include <stdexcept> #include <vereign/sqlite/connection.hh> -#include <vereign/sqlite/errors.hh> +#include <vereign/sqlite/errors.hh> #include <vereign/core/scope_guard.hh> #include <fmt/format.h> #include <sqlite3.h> -#include <memory> +#include <iostream> namespace vereign::sqlite { -Transaction::Transaction(sqlite3* db) - : finished_{false}, - db_{db} -{} - -Transaction::Transaction(Transaction&& other) - : finished_{other.finished_}, - db_{other.db_} +Connection::Connection(const std::string& path) + : db_{nullptr} { - other.finished_ = true; -} - -auto Transaction::operator=(Transaction&& other) -> Transaction& { - if (!finished_) { - throw std::runtime_error{"cannot move assign to non finished transaction"}; + auto rc = sqlite3_open(path.data(), &db_); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("open db failed, err: {}", sqlite3_errmsg(db_))}; } +} - db_ = other.db_; - finished_ = other.finished_; - other.finished_ = true; - - return *this; +Connection::~Connection() noexcept { + sqlite3_close(db_); } -void Transaction::Commit() { +void Connection::BeginExplicitTransaction() { char* errMsg = nullptr; auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); }); - - auto rc = sqlite3_exec(db_, "COMMIT", nullptr, nullptr, &errMsg); + auto rc = sqlite3_exec(db_, "BEGIN EXCLUSIVE", nullptr, nullptr, &errMsg); if (rc != SQLITE_OK) { - throw Error(rc, fmt::format("commit transaction failed, err: {}", errMsg)); + throw Error(rc, fmt::format("starting transaction failed, err: {}", errMsg)); } - - finished_ = true; } -void Transaction::Rollback() { +void Connection::Commit() { char* errMsg = nullptr; auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); }); - auto rc = sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, &errMsg); - if (rc != SQLITE_OK) { - throw Error(rc, fmt::format("rollback transaction failed, err: {}", errMsg)); - } - - finished_ = true; -} - -Transaction::~Transaction() { - if (!finished_) { - Rollback(); - } -} - -Statement::Statement(sqlite3* db, sqlite3_stmt* stmt) - : db_{db}, - stmt_{stmt} -{} - -void Statement::Finalize() { - sqlite3_finalize(stmt_); -} - -Statement::~Statement() { - sqlite3_finalize(stmt_); -} - -void Statement::BindBlob(int index, bytes::View blob) { - auto rc = sqlite3_bind_blob64(stmt_, index, blob.CharData(), blob.Size(), SQLITE_STATIC); - if (rc != SQLITE_OK) { - throw Error{rc, fmt::format("bind blob parameter failed, err: {}", sqlite3_errmsg(db_))}; - } -} - -void Statement::BindText(int index, const std::string& text) { - auto rc = sqlite3_bind_text(stmt_, index, text.data(), text.size(), SQLITE_STATIC); - if (rc != SQLITE_OK) { - throw Error{rc, fmt::format("bind text parameter failed, err: {}", sqlite3_errmsg(db_))}; - } -} - -auto Statement::Step() -> bool { - auto rc = sqlite3_step(stmt_); - switch (rc) { - case SQLITE_DONE: - return true; - case SQLITE_ROW: - return false; - default: - throw Error{rc, fmt::format("executing statement failed, err: {}", sqlite3_errmsg(db_))}; - } -} - -auto Statement::GetColumnBlob(int index) -> bytes::View { - auto size = sqlite3_column_bytes(stmt_, index); - auto blob = sqlite3_column_blob(stmt_, index); - - return bytes::View{blob, static_cast<size_t>(size)}; -} - -auto Statement::GetColumnText(int index) -> std::string_view { - std::size_t size = sqlite3_column_bytes(stmt_, index); - auto blob = sqlite3_column_text(stmt_, index); - - return std::string_view{reinterpret_cast<const char*>(blob), size}; -} - -void Statement::ResetAndClearBindings() { - auto rc = sqlite3_clear_bindings(stmt_); - if (rc != SQLITE_OK) { - throw Error{rc, fmt::format("statement reset bindings failed, err: {}", sqlite3_errmsg(db_))}; - } - - Reset(); -} - -void Statement::Reset() { - auto rc = sqlite3_reset(stmt_); - if (rc != SQLITE_OK) { - throw Error{rc, fmt::format("statement reset failed, err: {}", sqlite3_errmsg(db_))}; - } -} - -Connection::Connection(const std::string& path) - : db_{nullptr} -{ - auto rc = sqlite3_open(path.data(), &db_); + auto rc = sqlite3_exec(db_, "COMMIT", nullptr, nullptr, &errMsg); if (rc != SQLITE_OK) { - throw Error{rc, fmt::format("open db failed, err: {}", sqlite3_errmsg(db_))}; + throw Error(rc, fmt::format("commit transaction failed, err: {}", errMsg)); } } -Connection::~Connection() noexcept { - sqlite3_close(db_); -} - -auto Connection::BeginExplicitTransaction() -> Transaction { +void Connection::Rollback() { char* errMsg = nullptr; auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); }); - auto rc = sqlite3_exec(db_, "BEGIN EXCLUSIVE", nullptr, nullptr, &errMsg); + + auto rc = sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, &errMsg); if (rc != SQLITE_OK) { - throw Error(rc, fmt::format("starting transaction failed, err: {}", errMsg)); + throw Error(rc, fmt::format("rollback transaction failed, err: {}", errMsg)); } - - return Transaction{db_}; } - void Connection::Execute(const std::string& sql) { char* errMsg = nullptr; auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); }); diff --git a/cpp/src/vereign/sqlite/connection.hh b/cpp/src/vereign/sqlite/connection.hh index 86bfd6f839847984f26d0eae3d8ba05ef1f1fbb7..d1134045dc4c2434ff20ad5b2841e58a51c5c253 100644 --- a/cpp/src/vereign/sqlite/connection.hh +++ b/cpp/src/vereign/sqlite/connection.hh @@ -1,63 +1,13 @@ #ifndef __VEREIGN_SQLITE_CONNECTION_HH #define __VEREIGN_SQLITE_CONNECTION_HH -#include <vereign/bytes/view.hh> -#include <vereign/bytes/bytes.hh> - -#include <string> +#include <vereign/sqlite/statement.hh> struct sqlite3; struct sqlite3_stmt; namespace vereign::sqlite { -class Transaction { -private: - friend class Connection; - explicit Transaction(sqlite3* db); - -public: - void Commit(); - void Rollback(); - - Transaction(Transaction&&); - auto operator=(Transaction&&) -> Transaction&; - ~Transaction(); - - Transaction(const Transaction&) = delete; - auto operator=(const Transaction&) -> Transaction& = delete; - -private: - bool finished_; - sqlite3* db_; -}; - -class Statement { -private: - friend class Connection; - - explicit Statement(sqlite3* db, sqlite3_stmt* stmt); - -public: - ~Statement(); - - void BindBlob(int index, bytes::View blob); - void BindText(int index, const std::string& text); - - auto Step() -> bool; - - auto GetColumnBlob(int index) -> bytes::View; - auto GetColumnText(int index) -> std::string_view; - - void Reset(); - void ResetAndClearBindings(); - void Finalize(); - -private: - sqlite3* db_; - sqlite3_stmt* stmt_; -}; - class Connection { public: Connection(const std::string& path); @@ -66,9 +16,13 @@ public: Connection(const Connection&) = delete; auto operator=(const Connection&) -> Connection& = delete; - auto BeginExplicitTransaction() -> Transaction; + void BeginExplicitTransaction(); + void Commit(); + void Rollback(); + void Execute(const std::string& sql); auto Prepare(const std::string& sql) -> Statement; + private: sqlite3* db_; }; diff --git a/cpp/src/vereign/sqlite/errors.hh b/cpp/src/vereign/sqlite/errors.hh index 14fa1d81431e2cb4dfe555d292b1952f10bd80c7..00622311b42921a41500d7d7b3cc1f5b0e54f9a2 100644 --- a/cpp/src/vereign/sqlite/errors.hh +++ b/cpp/src/vereign/sqlite/errors.hh @@ -5,15 +5,12 @@ namespace vereign::sqlite { -class Error : virtual std::exception { +class Error : public std::runtime_error { public: - Error(int code, std::string&& msg) - : code_{code}, - msg_{std::move(msg)} - {} - - auto what() const noexcept -> const char* override { - return msg_.data(); + Error(int code, const std::string& msg) + : std::runtime_error(msg), + code_{code} + { } auto code() const noexcept -> int { @@ -22,7 +19,6 @@ public: private: int code_; - std::string msg_; }; } // namespace vereign::sqlite diff --git a/cpp/src/vereign/sqlite/statement.cc b/cpp/src/vereign/sqlite/statement.cc new file mode 100644 index 0000000000000000000000000000000000000000..0a3d00795383e4498580c1a56265b1ce55d87918 --- /dev/null +++ b/cpp/src/vereign/sqlite/statement.cc @@ -0,0 +1,78 @@ +#include <vereign/sqlite/statement.hh> +#include <vereign/sqlite/errors.hh> + +#include <fmt/format.h> +#include <sqlite3.h> + +namespace vereign::sqlite { + +Statement::Statement(sqlite3* db, sqlite3_stmt* stmt) + : db_{db}, + stmt_{stmt} +{} + +void Statement::Finalize() { + sqlite3_finalize(stmt_); +} + +Statement::~Statement() { + sqlite3_finalize(stmt_); +} + +void Statement::BindBlob(int index, bytes::View blob) { + auto rc = sqlite3_bind_blob64(stmt_, index, blob.CharData(), blob.Size(), SQLITE_STATIC); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("bind blob parameter failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +void Statement::BindText(int index, const std::string& text) { + auto rc = sqlite3_bind_text(stmt_, index, text.data(), text.size(), SQLITE_STATIC); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("bind text parameter failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +auto Statement::Step() -> bool { + auto rc = sqlite3_step(stmt_); + switch (rc) { + case SQLITE_DONE: + return true; + case SQLITE_ROW: + return false; + default: + throw Error{rc, fmt::format("executing statement failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +auto Statement::GetColumnBlob(int index) -> bytes::View { + auto size = sqlite3_column_bytes(stmt_, index); + auto blob = sqlite3_column_blob(stmt_, index); + + return bytes::View{blob, static_cast<size_t>(size)}; +} + +auto Statement::GetColumnText(int index) -> std::string_view { + std::size_t size = sqlite3_column_bytes(stmt_, index); + auto blob = sqlite3_column_text(stmt_, index); + + return std::string_view{reinterpret_cast<const char*>(blob), size}; +} + +void Statement::ResetAndClearBindings() { + auto rc = sqlite3_clear_bindings(stmt_); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("statement reset bindings failed, err: {}", sqlite3_errmsg(db_))}; + } + + Reset(); +} + +void Statement::Reset() { + auto rc = sqlite3_reset(stmt_); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("statement reset failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +} // namespace vereign::sqlite diff --git a/cpp/src/vereign/sqlite/statement.hh b/cpp/src/vereign/sqlite/statement.hh new file mode 100644 index 0000000000000000000000000000000000000000..2b8c336d735a5ec33eb673dffa8df0ef87cce676 --- /dev/null +++ b/cpp/src/vereign/sqlite/statement.hh @@ -0,0 +1,42 @@ +#ifndef __VEREIGN_SQLITE_STATEMENT_HH +#define __VEREIGN_SQLITE_STATEMENT_HH + +#include <vereign/bytes/view.hh> +#include <vereign/bytes/bytes.hh> + +#include <string> + +struct sqlite3; +struct sqlite3_stmt; + +namespace vereign::sqlite { + +class Statement { +private: + friend class Connection; + + explicit Statement(sqlite3* db, sqlite3_stmt* stmt); + +public: + ~Statement(); + + void BindBlob(int index, bytes::View blob); + void BindText(int index, const std::string& text); + + auto Step() -> bool; + + auto GetColumnBlob(int index) -> bytes::View; + auto GetColumnText(int index) -> std::string_view; + + void Reset(); + void ResetAndClearBindings(); + void Finalize(); + +private: + sqlite3* db_; + sqlite3_stmt* stmt_; +}; + +} // namespace vereign::sqlite + +#endif // __VEREIGN_SQLITE_STATEMENT_HH diff --git a/cpp/tests/util/error.hh b/cpp/tests/util/error.hh new file mode 100644 index 0000000000000000000000000000000000000000..85d7951d333c60aa7c222cab272ff6737798fd95 --- /dev/null +++ b/cpp/tests/util/error.hh @@ -0,0 +1,22 @@ +#ifndef __VEREIGN_TEST_UTIL_ERROR_HH +#define __VEREIGN_TEST_UTIL_ERROR_HH + +#include <optional> + +namespace vereign::test { + +template <class Error, class Fun> +auto CatchError(Fun fn) -> std::optional<Error> { + try { + fn(); + + return {}; + } catch (const Error& err) { + + return err; + } +} + +} // namespace test + +#endif // __VEREIGN_TEST_UTIL_ERROR_HH diff --git a/cpp/tests/vereign/CMakeLists.txt b/cpp/tests/vereign/CMakeLists.txt index 172c4032825569568b6a6cba042d4cde96a91360..3994b8bad658abf6c3d90ca345bf4f88ed7a9964 100644 --- a/cpp/tests/vereign/CMakeLists.txt +++ b/cpp/tests/vereign/CMakeLists.txt @@ -28,6 +28,8 @@ list(APPEND TESTS_SRC restapi/client_test.cc restapi/client_session_test.cc + kvstore/lock_test.cc + kvstore/sqlite_storage_test.cc kvstore/crypto_storage_test.cc identity/provider_test.cc diff --git a/cpp/tests/vereign/kvstore/crypto_storage_test.cc b/cpp/tests/vereign/kvstore/crypto_storage_test.cc index b381e8e373d18c05e4525f4458eab50f88dd5e98..af314d9690e521b349f9ed7696f8a1f72a072812 100644 --- a/cpp/tests/vereign/kvstore/crypto_storage_test.cc +++ b/cpp/tests/vereign/kvstore/crypto_storage_test.cc @@ -17,6 +17,7 @@ TEST_CASE("CryptoStorage::Reset", "[vereign/kvstore]") { auto storage_path = core::TempFilePath("test_db_"); core::RemoveFileGuard rm{storage_path}; + // put value { auto kvstorage = kvstore::SqliteStorage(storage_path.string()); kvstore::CryptoStorage storage{kvstorage}; @@ -26,6 +27,7 @@ TEST_CASE("CryptoStorage::Reset", "[vereign/kvstore]") { storage.PutBytes("test", bytes::View(v)); } + // with another storage instance get the value { auto kvstorage = kvstore::SqliteStorage(storage_path.string()); kvstore::CryptoStorage storage{kvstorage}; @@ -36,28 +38,25 @@ TEST_CASE("CryptoStorage::Reset", "[vereign/kvstore]") { CHECK(v.View().String() == "test value"); } +} + +TEST_CASE("CryptoStorage::PutBytes", "[vereign/kvstore]") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; bytes::Buffer big_value{100000}; crypto::Rand(big_value); - { - auto kvstorage = kvstore::SqliteStorage(storage_path.string()); - kvstore::CryptoStorage storage{kvstorage}; + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + kvstore::CryptoStorage storage{kvstorage}; - storage.Reset("foo"); - storage.PutBytes("test", big_value.View()); - } + storage.Reset("foo"); + storage.PutBytes("test", big_value.View()); - { - auto kvstorage = kvstore::SqliteStorage(storage_path.string()); - kvstore::CryptoStorage storage{kvstorage}; - - bytes::Buffer v; - storage.Open("foo"); - storage.GetBytes("test", v); + bytes::Buffer v; + storage.GetBytes("test", v); - REQUIRE(v.Size() == big_value.Size()); + REQUIRE(v.Size() == big_value.Size()); - auto cmp = std::memcmp(v.View().Data(), big_value.View().Data(), v.Size()); - CHECK(cmp == 0); - } + auto cmp = std::memcmp(v.View().Data(), big_value.View().Data(), v.Size()); + CHECK(cmp == 0); } diff --git a/cpp/tests/vereign/kvstore/lock_test.cc b/cpp/tests/vereign/kvstore/lock_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..9db7bef8463f09e92b1397d829eb798394d4bb98 --- /dev/null +++ b/cpp/tests/vereign/kvstore/lock_test.cc @@ -0,0 +1,62 @@ +#include <vereign/kvstore/sqlite_storage.hh> + +#include <vereign/kvstore/lock.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/core/fs.hh> +#include <vereign/core/lock_guard.hh> +#include <vereign/core/scope_guard.hh> +#include <vereign/core/temp.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/sqlite/errors.hh> +#include <util/error.hh> +#include <sqlite3.h> + +#include <catch2/catch.hpp> +#include <boost/filesystem.hpp> +#include <thread> +#include <chrono> +#include <limits> +#include <optional> + +using namespace vereign; + +TEST_CASE("kvstore::Lock", "[vereign/kvstore]") { + + SECTION("when the lock is released within the allowed retrials, the lock succeeds") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; + + auto foo_storage = kvstore::SqliteStorage(storage_path.string()); + auto bar_storage = kvstore::SqliteStorage(storage_path.string()); + + foo_storage.Lock(); + + bool err = false; + auto th = std::thread{[&bar_storage, &err]() { + try { + kvstore::Lock l{bar_storage, std::numeric_limits<int>::max(), std::chrono::milliseconds{10}}; + } catch (...) { + err = true; + } + }}; + + foo_storage.Unlock(); + th.join(); + } + + SECTION("when the lock is not released within the allowed retrials, the lock fails") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; + + auto foo_storage = kvstore::SqliteStorage(storage_path.string()); + auto bar_storage = kvstore::SqliteStorage(storage_path.string()); + + foo_storage.Lock(); + + CHECK_THROWS_AS( + kvstore::Lock(bar_storage, 2, std::chrono::milliseconds{1}), + kvstore::LockError + ); + } +} diff --git a/cpp/tests/vereign/kvstore/sqlite_storage_test.cc b/cpp/tests/vereign/kvstore/sqlite_storage_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..f8408575d564777dd639fa09c9091a7e11000ef4 --- /dev/null +++ b/cpp/tests/vereign/kvstore/sqlite_storage_test.cc @@ -0,0 +1,97 @@ +#include <vereign/kvstore/sqlite_storage.hh> + +#include <vereign/kvstore/lock.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/core/fs.hh> +#include <vereign/core/scope_guard.hh> +#include <vereign/core/temp.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/sqlite/errors.hh> +#include <util/error.hh> +#include <sqlite3.h> + +#include <catch2/catch.hpp> +#include <boost/filesystem.hpp> +#include <thread> +#include <optional> + +using namespace vereign; + + +TEST_CASE("kvstore::SqliteStorage::DeleteAll", "[vereign/kvstore]") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; + + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + + kvstorage.PutInt64("foo", 42); + kvstorage.PutInt64("bar", 422); + + CHECK(kvstorage.GetInt64("foo") == 42); + CHECK(kvstorage.GetInt64("bar") == 422); + + kvstorage.DeleteAll(); + + CHECK(kvstorage.GetInt64("foo") == 0); + CHECK(kvstorage.GetInt64("bar") == 0); +} + +TEST_CASE("kvstore::SqliteStorage::Lock", "[vereign/kvstore]") { + + SECTION("when locked using lock guard, it must unlock on scope exit") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; + + { + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + + kvstore::Lock l{kvstorage}; + + kvstorage.PutInt64("foo", 42); + kvstorage.PutInt64("bar", 422); + } + + { + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + + kvstore::Lock l{kvstorage}; + + CHECK(kvstorage.GetInt64("foo") == 42); + CHECK(kvstorage.GetInt64("bar") == 422); + } + } + + SECTION("when locked, it must unlock on scope exit") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; + + { + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + kvstorage.Lock(); + + kvstorage.PutInt64("foo", 42); + kvstorage.PutInt64("bar", 422); + } + + { + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + kvstorage.Lock(); + + CHECK(kvstorage.GetInt64("foo") == 42); + CHECK(kvstorage.GetInt64("bar") == 422); + } + } + + SECTION("when the storage is already locked, it must fail with LockError") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; + + auto foo_storage = kvstore::SqliteStorage(storage_path.string());; + auto bar_storage = kvstore::SqliteStorage(storage_path.string()); + + foo_storage.Lock(); + + CHECK_THROWS_AS(bar_storage.Lock(), kvstore::LockError); + } +}