From 54f149fc1858cac7f560b9a6140e5412e7f68acb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Mon, 10 Jul 2023 16:01:45 -0400
Subject: [PATCH] conversationrepository: protect operation

+ Avoid concurrent operations on same repository
+ Reset HEAD to always get a clean commit

GitLab: #931
Change-Id: If417c06934e2acab3382481718dc0bc46f40e3f9
---
 src/jamidht/conversationrepository.cpp        | 76 ++++++++++++++++---
 src/jamidht/conversationrepository.h          |  3 +
 .../conversation/conversationcommon.cpp       |  6 ++
 3 files changed, 76 insertions(+), 9 deletions(-)

diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp
index cbf51c237d..35b613f080 100644
--- a/src/jamidht/conversationrepository.cpp
+++ b/src/jamidht/conversationrepository.cpp
@@ -40,6 +40,10 @@ constexpr size_t MAX_FETCH_SIZE {256 * 1024 * 1024}; // 256Mb
 
 namespace jami {
 
+#ifdef LIBJAMI_TESTABLE
+bool ConversationRepository::DISABLE_RESET = false;
+#endif
+
 static const std::regex regex_display_name("<|>");
 
 inline std::string_view
@@ -158,9 +162,11 @@ public:
 
     bool add(const std::string& path);
     void addUserDevice();
+    void resetHard();
     // Verify that the device in the repository is still valid
     bool validateDevice();
     std::string commit(const std::string& msg, bool verifyDevice = true);
+    std::string commitMessage(const std::string& msg, bool verifyDevice = true);
     ConversationMode mode() const;
 
     // NOTE! GitDiff needs to be deteleted before repo
@@ -419,6 +425,8 @@ public:
 
         return true;
     }
+
+    std::mutex opMtx_; // Mutex for operations
 };
 
 /////////////////////////////////////////////////////////////////////////////////
@@ -2877,6 +2885,8 @@ ConversationRepository::id() const
 std::string
 ConversationRepository::addMember(const std::string& uri)
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     auto account = pimpl_->account_.lock();
     auto repo = pimpl_->repository();
     if (!account or not repo)
@@ -2980,6 +2990,8 @@ ConversationRepository::amend(const std::string& id, const std::string& msg)
 bool
 ConversationRepository::fetch(const std::string& remoteDeviceId)
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     // Fetch distant repository
     git_remote* remote_ptr = nullptr;
     git_fetch_options fetch_opts;
@@ -3098,11 +3110,39 @@ ConversationRepository::Impl::addUserDevice()
     }
 }
 
+void
+ConversationRepository::Impl::resetHard()
+{
+#ifdef LIBJAMI_TESTABLE
+    if (DISABLE_RESET)
+        return;
+#endif
+    auto repo = repository();
+    if (!repo)
+        return;
+    git_object *head_commit_obj = nullptr;
+    auto error = git_revparse_single(&head_commit_obj, repo.get(), "HEAD");
+    if (error < 0) {
+        JAMI_ERROR("Could not get HEAD commit");
+        return;
+    }
+    GitObject target {head_commit_obj, git_object_free};
+    git_reset(repo.get(), head_commit_obj, GIT_RESET_HARD, nullptr);
+}
+
 std::string
 ConversationRepository::commitMessage(const std::string& msg, bool verifyDevice)
 {
-    pimpl_->addUserDevice();
-    return pimpl_->commit(msg, verifyDevice);
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
+    return pimpl_->commitMessage(msg, verifyDevice);
+}
+
+std::string
+ConversationRepository::Impl::commitMessage(const std::string& msg, bool verifyDevice)
+{
+    addUserDevice();
+    return commit(msg, verifyDevice);
 }
 
 std::vector<std::string>
@@ -3145,6 +3185,8 @@ ConversationRepository::getCommit(const std::string& commitId, bool logIfNotFoun
 std::pair<bool, std::string>
 ConversationRepository::merge(const std::string& merge_id, bool force)
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     // First, the repository must be in a clean state
     auto repo = pimpl_->repository();
     if (!repo) {
@@ -3153,8 +3195,12 @@ ConversationRepository::merge(const std::string& merge_id, bool force)
     }
     int state = git_repository_state(repo.get());
     if (state != GIT_REPOSITORY_STATE_NONE) {
-        JAMI_ERROR("Merge operation aborted: repository is in unexpected state %d", state);
-        return {false, ""};
+        pimpl_->resetHard();
+        int state = git_repository_state(repo.get());
+        if (state != GIT_REPOSITORY_STATE_NONE) {
+            JAMI_ERROR("Merge operation aborted: repository is in unexpected state {}", state);
+            return {false, ""};
+        }
     }
     // Checkout main (to do a `git_merge branch`)
     if (git_repository_set_head(repo.get(), "refs/heads/main") < 0) {
@@ -3295,6 +3341,8 @@ ConversationRepository::changedFiles(std::string_view diffStats)
 std::string
 ConversationRepository::join()
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     // Check that not already member
     auto repo = pimpl_->repository();
     if (!repo)
@@ -3361,12 +3409,14 @@ ConversationRepository::join()
         pimpl_->saveMembers();
     }
 
-    return commitMessage(Json::writeString(wbuilder, json));
+    return pimpl_->commitMessage(Json::writeString(wbuilder, json));
 }
 
 std::string
 ConversationRepository::leave()
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     // TODO simplify
     auto account = pimpl_->account_.lock();
     auto repo = pimpl_->repository();
@@ -3458,6 +3508,8 @@ ConversationRepository::mode() const
 std::string
 ConversationRepository::voteKick(const std::string& uri, const std::string& type)
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     auto repo = pimpl_->repository();
     auto account = pimpl_->account_.lock();
     if (!account || !repo)
@@ -3502,12 +3554,14 @@ ConversationRepository::voteKick(const std::string& uri, const std::string& type
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
-    return commitMessage(Json::writeString(wbuilder, json));
+    return pimpl_->commitMessage(Json::writeString(wbuilder, json));
 }
 
 std::string
 ConversationRepository::voteUnban(const std::string& uri, const std::string_view type)
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     auto repo = pimpl_->repository();
     auto account = pimpl_->account_.lock();
     if (!account || !repo)
@@ -3542,7 +3596,7 @@ ConversationRepository::voteUnban(const std::string& uri, const std::string_view
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
-    return commitMessage(Json::writeString(wbuilder, json));
+    return pimpl_->commitMessage(Json::writeString(wbuilder, json));
 }
 
 bool
@@ -3648,6 +3702,8 @@ ConversationRepository::resolveVote(const std::string& uri,
                                     const std::string_view type,
                                     const std::string& voteType)
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     // Count ratio admin/votes
     auto nbAdmins = 0, nbVotes = 0;
     // For each admin, check if voted
@@ -3693,7 +3749,7 @@ ConversationRepository::resolveVote(const std::string& uri,
         Json::StreamWriterBuilder wbuilder;
         wbuilder["commentStyle"] = "None";
         wbuilder["indentation"] = "";
-        return commitMessage(Json::writeString(wbuilder, json));
+        return pimpl_->commitMessage(Json::writeString(wbuilder, json));
     }
 
     // If vote nok
@@ -3809,6 +3865,8 @@ ConversationRepository::uriFromDevice(const std::string& deviceId) const
 std::string
 ConversationRepository::updateInfos(const std::map<std::string, std::string>& profile)
 {
+    std::lock_guard lkOp(pimpl_->opMtx_);
+    pimpl_->resetHard();
     auto account = pimpl_->account_.lock();
     if (!account)
         return {};
@@ -3890,7 +3948,7 @@ ConversationRepository::updateInfos(const std::map<std::string, std::string>& pr
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
 
-    return commitMessage(Json::writeString(wbuilder, json));
+    return pimpl_->commitMessage(Json::writeString(wbuilder, json));
 }
 
 std::map<std::string, std::string>
diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h
index 41c0872d7a..7cb8f81672 100644
--- a/src/jamidht/conversationrepository.h
+++ b/src/jamidht/conversationrepository.h
@@ -141,6 +141,9 @@ using PostConditionCb
 class LIBJAMI_TESTABLE ConversationRepository
 {
 public:
+    #ifdef LIBJAMI_TESTABLE
+    static bool DISABLE_RESET; // Some tests inject bad files so resetHard() will break the test
+    #endif
     /**
      * Creates a new repository, with initial files, where the first commit hash is the conversation id
      * @param account       The related account
diff --git a/test/unitTest/conversation/conversationcommon.cpp b/test/unitTest/conversation/conversationcommon.cpp
index ecfe4c85c9..31c7f5284f 100644
--- a/test/unitTest/conversation/conversationcommon.cpp
+++ b/test/unitTest/conversation/conversationcommon.cpp
@@ -51,6 +51,7 @@ addVote(std::shared_ptr<JamiAccount> account,
         const std::string& votedUri,
         const std::string& content)
 {
+    ConversationRepository::DISABLE_RESET = true;
     auto repoPath = fileutils::get_data_dir() / account->getAccountID()
                     / "conversations" / convId;
     auto voteDirectory = repoPath / "votes" / "members";
@@ -80,6 +81,7 @@ simulateRemoval(std::shared_ptr<JamiAccount> account,
                 const std::string& convId,
                 const std::string& votedUri)
 {
+    ConversationRepository::DISABLE_RESET = true;
     auto repoPath = fileutils::get_data_dir() / account->getAccountID()
                     / "conversations" / convId;
     auto memberFile = repoPath / "members" / (votedUri + ".crt");
@@ -125,6 +127,7 @@ addFile(std::shared_ptr<JamiAccount> account,
         const std::string& relativePath,
         const std::string& content)
 {
+    ConversationRepository::DISABLE_RESET = true;
     auto repoPath = fileutils::get_data_dir() / account->getAccountID()
                     / "conversations" / convId;
     // Add file
@@ -155,6 +158,7 @@ addFile(std::shared_ptr<JamiAccount> account,
 void
 addAll(std::shared_ptr<JamiAccount> account, const std::string& convId)
 {
+    ConversationRepository::DISABLE_RESET = true;
     auto repoPath = fileutils::get_data_dir() / account->getAccountID()
                     / "conversations" / convId;
 
@@ -177,6 +181,7 @@ addAll(std::shared_ptr<JamiAccount> account, const std::string& convId)
 void
 commit(std::shared_ptr<JamiAccount> account, const std::string& convId, Json::Value& message)
 {
+    ConversationRepository::DISABLE_RESET = true;
     ConversationRepository cr(account->weak(), convId);
 
     Json::StreamWriterBuilder wbuilder;
@@ -188,6 +193,7 @@ commit(std::shared_ptr<JamiAccount> account, const std::string& convId, Json::Va
 std::string
 commitInRepo(const std::string& path, std::shared_ptr<JamiAccount> account, const std::string& msg)
 {
+    ConversationRepository::DISABLE_RESET = true;
     auto deviceId = std::string(account->currentDeviceId());
     auto name = account->getDisplayName();
     if (name.empty())
-- 
GitLab