LCOV - code coverage report
Current view: top level - foo/src/jamidht - conversationrepository.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1900 2580 73.6 %
Date: 2026-04-01 09:29:43 Functions: 228 638 35.7 %

          Line data    Source code
       1             : /*
       2             :  *  Copyright (C) 2004-2026 Savoir-faire Linux Inc.
       3             :  *
       4             :  *  This program is free software: you can redistribute it and/or modify
       5             :  *  it under the terms of the GNU General Public License as published by
       6             :  *  the Free Software Foundation, either version 3 of the License, or
       7             :  *  (at your option) any later version.
       8             :  *
       9             :  *  This program is distributed in the hope that it will be useful,
      10             :  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12             :  *  GNU General Public License for more details.
      13             :  *
      14             :  *  You should have received a copy of the GNU General Public License
      15             :  *  along with this program. If not, see <https://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "conversationrepository.h"
      19             : 
      20             : #include "account_const.h"
      21             : #include "base64.h"
      22             : #include "jamiaccount.h"
      23             : #include "fileutils.h"
      24             : #include "gittransport.h"
      25             : #include "string_utils.h"
      26             : #include "client/jami_signal.h"
      27             : #include "vcard.h"
      28             : #include "json_utils.h"
      29             : #include "fileutils.h"
      30             : #include "logger.h"
      31             : #include "jami/conversation_interface.h"
      32             : 
      33             : #include <opendht/crypto.h>
      34             : 
      35             : #include <git2/blob.h>
      36             : #include <git2/buffer.h>
      37             : #include <git2/commit.h>
      38             : #include <git2/deprecated.h>
      39             : #include <git2/refs.h>
      40             : #include <git2/object.h>
      41             : #include <git2/indexer.h>
      42             : #include <git2/remote.h>
      43             : #include <git2/merge.h>
      44             : #include <git2/diff.h>
      45             : 
      46             : #include <algorithm>
      47             : #include <iterator>
      48             : #include <ctime>
      49             : #include <fstream>
      50             : #include <future>
      51             : #include <json/json.h>
      52             : #include <regex>
      53             : #include <exception>
      54             : #include <optional>
      55             : #include <memory>
      56             : #include <cstdint>
      57             : #include <utility>
      58             : 
      59             : using namespace std::string_view_literals;
      60             : constexpr auto DIFF_REGEX = " +\\| +[0-9]+.*"sv;
      61             : constexpr size_t MAX_FETCH_SIZE {256 * 1024 * 1024}; // 256Mb
      62             : 
      63             : namespace jami {
      64             : 
      65             : #ifdef LIBJAMI_TEST
      66             : bool ConversationRepository::DISABLE_RESET = false;
      67             : bool ConversationRepository::FETCH_FROM_LOCAL_REPOS = false;
      68             : #endif
      69             : 
      70             : static const std::regex regex_display_name("<|>");
      71             : 
      72             : inline std::string_view
      73        6159 : as_view(const git_blob* blob)
      74             : {
      75        6159 :     return std::string_view(static_cast<const char*>(git_blob_rawcontent(blob)), git_blob_rawsize(blob));
      76             : }
      77             : inline std::string_view
      78        6161 : as_view(const GitObject& blob)
      79             : {
      80        6161 :     return as_view(reinterpret_cast<git_blob*>(blob.get()));
      81             : }
      82             : 
      83             : class ConversationRepository::Impl
      84             : {
      85             : public:
      86         427 :     Impl(const std::shared_ptr<JamiAccount>& account, const std::string& id)
      87         427 :         : account_(account)
      88         427 :         , id_(id)
      89         427 :         , accountId_(account->getAccountID())
      90         427 :         , userId_(account->getUsername())
      91         854 :         , deviceId_(account->currentDeviceId())
      92             :     {
      93         427 :         conversationDataPath_ = fileutils::get_data_dir() / accountId_ / "conversation_data" / id_;
      94         427 :         membersCache_ = conversationDataPath_ / "members";
      95         427 :         checkLocks();
      96         427 :         loadMembers();
      97         427 :         if (members_.empty()) {
      98         413 :             initMembers();
      99             :         }
     100         436 :     }
     101             : 
     102         427 :     void checkLocks()
     103             :     {
     104         427 :         auto repo = repository();
     105         427 :         if (!repo)
     106           0 :             throw std::logic_error("Invalid git repository");
     107             : 
     108         427 :         std::filesystem::path repoPath = git_repository_path(repo.get());
     109         427 :         std::error_code ec;
     110             : 
     111         427 :         auto indexPath = std::filesystem::path(repoPath / "index.lock");
     112         427 :         if (std::filesystem::exists(indexPath, ec)) {
     113           0 :             JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked, removing lock {}",
     114             :                          accountId_,
     115             :                          id_,
     116             :                          indexPath);
     117           0 :             std::filesystem::remove(indexPath, ec);
     118           0 :             if (ec)
     119           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
     120             :                            accountId_,
     121             :                            id_,
     122             :                            indexPath,
     123             :                            ec.message());
     124             :         }
     125             : 
     126         854 :         auto refPath = std::filesystem::path(repoPath / "refs" / "heads" / "main.lock");
     127         427 :         if (std::filesystem::exists(refPath)) {
     128           0 :             JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked, removing lock {}",
     129             :                          accountId_,
     130             :                          id_,
     131             :                          refPath);
     132           0 :             std::filesystem::remove(refPath, ec);
     133           0 :             if (ec)
     134           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
     135             :                            accountId_,
     136             :                            id_,
     137             :                            refPath,
     138             :                            ec.message());
     139             :         }
     140             : 
     141         854 :         auto remotePath = std::filesystem::path(repoPath / "refs" / "remotes");
     142        1486 :         for (const auto& fileIt : std::filesystem::directory_iterator(remotePath, ec)) {
     143         205 :             auto refPath = fileIt.path() / "main.lock";
     144         205 :             if (std::filesystem::exists(refPath, ec)) {
     145           0 :                 JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked for remote {}, removing lock",
     146             :                              accountId_,
     147             :                              id_,
     148             :                              fileIt.path().filename());
     149           0 :                 std::filesystem::remove(refPath, ec);
     150           0 :                 if (ec)
     151           0 :                     JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
     152             :                                accountId_,
     153             :                                id_,
     154             :                                refPath,
     155             :                                ec.message());
     156             :             }
     157         632 :         }
     158             : 
     159         427 :         auto err = git_repository_state_cleanup(repo.get());
     160         427 :         if (err < 0) {
     161           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to clean up the repository: {}",
     162             :                        accountId_,
     163             :                        id_,
     164             :                        git_error_last()->message);
     165             :         }
     166         427 :     }
     167             : 
     168         427 :     void loadMembers()
     169             :     {
     170             :         try {
     171             :             // read file
     172         839 :             auto file = fileutils::loadFile(membersCache_);
     173             :             // load values
     174          14 :             msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
     175          14 :             std::lock_guard lk {membersMtx_};
     176          14 :             oh.get().convert(members_);
     177         426 :         } catch (const std::exception& e) {
     178         412 :         }
     179         427 :     }
     180             :     // Note: membersMtx_ needs to be locked when calling saveMembers
     181        1497 :     void saveMembers()
     182             :     {
     183        1497 :         std::ofstream file(membersCache_, std::ios::trunc | std::ios::binary);
     184        1497 :         msgpack::pack(file, members_);
     185             : 
     186        1497 :         if (onMembersChanged_) {
     187        1084 :             std::set<std::string> memberUris;
     188       14028 :             for (const auto& member : members_) {
     189       12947 :                 memberUris.emplace(member.uri);
     190             :             }
     191        1081 :             onMembersChanged_(memberUris);
     192        1084 :         }
     193        1497 :     }
     194             : 
     195             :     OnMembersChanged onMembersChanged_ {};
     196             : 
     197             :     // NOTE! We use temporary GitRepository to avoid keeping the file opened
     198             :     // TODO: check why git_remote_fetch() leaves pack-data opened
     199       46210 :     GitRepository repository() const
     200             :     {
     201       92419 :         auto path = fmt::format("{}/{}/conversations/{}", fileutils::get_data_dir().string(), accountId_, id_);
     202       46180 :         git_repository* repo = nullptr;
     203       46180 :         auto err = git_repository_open(&repo, path.c_str());
     204       46226 :         if (err < 0) {
     205           0 :             JAMI_ERROR("Unable to open Git repository: {} ({})", path, git_error_last()->message);
     206           0 :             return nullptr;
     207             :         }
     208       46226 :         return GitRepository(std::move(repo));
     209       46216 :     }
     210             : 
     211         514 :     std::string getDisplayName() const
     212             :     {
     213         514 :         auto shared = account_.lock();
     214         514 :         if (!shared)
     215           0 :             return {};
     216         514 :         auto name = shared->getDisplayName();
     217         514 :         if (name.empty())
     218           0 :             name = deviceId_;
     219         514 :         return std::regex_replace(name, regex_display_name, "");
     220         514 :     }
     221             : 
     222             :     GitSignature signature();
     223             :     bool mergeFastforward(const git_oid* target_oid, int is_unborn);
     224             :     std::string createMergeCommit(git_index* index, const std::string& wanted_ref);
     225             : 
     226             :     bool validCommits(const std::vector<ConversationCommit>& commits) const;
     227             :     bool checkValidUserDiff(const std::string& userDevice,
     228             :                             const std::string& commitId,
     229             :                             const std::string& parentId) const;
     230             :     bool checkVote(const std::string& userDevice, const std::string& commitId, const std::string& parentId) const;
     231             :     bool checkEdit(const std::string& userDevice, const ConversationCommit& commit) const;
     232             :     bool isValidUserAtCommit(const std::string& userDevice,
     233             :                              const std::string& commitId,
     234             :                              const git_buf& sig,
     235             :                              const git_buf& sig_data) const;
     236             :     bool checkInitialCommit(const std::string& userDevice,
     237             :                             const std::string& commitId,
     238             :                             const std::string& commitMsg) const;
     239             :     bool checkValidAdd(const std::string& userDevice,
     240             :                        const std::string& uriMember,
     241             :                        const std::string& commitid,
     242             :                        const std::string& parentId) const;
     243             :     bool checkValidJoins(const std::string& userDevice,
     244             :                          const std::string& uriMember,
     245             :                          const std::string& commitid,
     246             :                          const std::string& parentId) const;
     247             :     bool checkValidRemove(const std::string& userDevice,
     248             :                           const std::string& uriMember,
     249             :                           const std::string& commitid,
     250             :                           const std::string& parentId) const;
     251             :     bool checkValidVoteResolution(const std::string& userDevice,
     252             :                                   const std::string& uriMember,
     253             :                                   const std::string& commitId,
     254             :                                   const std::string& parentId,
     255             :                                   const std::string& voteType) const;
     256             :     bool checkValidProfileUpdate(const std::string& userDevice,
     257             :                                  const std::string& commitid,
     258             :                                  const std::string& parentId) const;
     259             :     bool checkValidMergeCommit(const std::string& mergeId, const std::vector<std::string>& parents) const;
     260             :     std::optional<std::set<std::string_view>> getDeltaPathsFromDiff(const GitDiff& diff) const;
     261             : 
     262             :     bool add(const std::string& path);
     263             :     void addUserDevice();
     264             :     void resetHard();
     265             :     // Verify that the device in the repository is still valid
     266             :     bool validateDevice();
     267             :     std::string commit(const std::string& msg, bool verifyDevice = true);
     268             :     std::string commitMessage(const std::string& msg, bool verifyDevice = true);
     269             :     ConversationMode mode() const;
     270             : 
     271             :     // NOTE! GitDiff needs to be deleted before repo
     272             :     GitDiff diff(git_repository* repo, const std::string& idNew, const std::string& idOld) const;
     273             :     std::string diffStats(const std::string& newId, const std::string& oldId) const;
     274             :     std::string diffStats(const GitDiff& diff) const;
     275             : 
     276             :     std::vector<ConversationCommit> behind(const std::string& from) const;
     277             :     void forEachCommit(PreConditionCb&& preCondition,
     278             :                        std::function<void(ConversationCommit&&)>&& emplaceCb,
     279             :                        PostConditionCb&& postCondition,
     280             :                        const std::string& from = "",
     281             :                        bool logIfNotFound = true) const;
     282             :     std::vector<ConversationCommit> log(const LogOptions& options) const;
     283             : 
     284             :     GitObject fileAtTree(const std::string& path, const GitTree& tree) const;
     285             :     GitObject memberCertificate(std::string_view memberUri, const GitTree& tree) const;
     286             :     // NOTE! GitDiff needs to be deleted before repo
     287             :     GitTree treeAtCommit(git_repository* repo, const std::string& commitId) const;
     288             : 
     289             :     std::vector<std::string> getInitialMembers() const;
     290             : 
     291             :     bool resolveBan(const std::string_view type, const std::string& uri);
     292             :     bool resolveUnban(const std::string_view type, const std::string& uri);
     293             : 
     294             :     std::weak_ptr<JamiAccount> account_;
     295             :     const std::string id_;
     296             :     const std::string accountId_;
     297             :     const std::string userId_;
     298             :     const std::string deviceId_;
     299             :     mutable std::optional<ConversationMode> mode_ {};
     300             : 
     301             :     // Members utils
     302             :     mutable std::mutex membersMtx_ {};
     303             :     std::vector<ConversationMember> members_ {};
     304             : 
     305        1353 :     std::vector<ConversationMember> members() const
     306             :     {
     307        1353 :         std::lock_guard lk(membersMtx_);
     308        2706 :         return members_;
     309        1353 :     }
     310             : 
     311             :     std::filesystem::path conversationDataPath_ {};
     312             :     std::filesystem::path membersCache_ {};
     313             : 
     314         513 :     std::map<std::string, std::vector<DeviceId>> devices(bool ignoreExpired = true) const
     315             :     {
     316         513 :         auto acc = account_.lock();
     317         513 :         auto repo = repository();
     318         513 :         if (!repo or !acc)
     319           0 :             return {};
     320         513 :         std::map<std::string, std::vector<DeviceId>> memberDevices;
     321         513 :         std::string deviceDir = fmt::format("{}devices/", git_repository_workdir(repo.get()));
     322         513 :         std::error_code ec;
     323        2562 :         for (const auto& fileIt : std::filesystem::directory_iterator(deviceDir, ec)) {
     324             :             try {
     325        2046 :                 auto cert = std::make_shared<dht::crypto::Certificate>(fileutils::loadFile(fileIt.path()));
     326        1023 :                 if (!cert)
     327           0 :                     continue;
     328        1023 :                 if (ignoreExpired && cert->getExpiration() < std::chrono::system_clock::now())
     329           1 :                     continue;
     330        1022 :                 auto issuerUid = cert->getIssuerUID();
     331        1022 :                 if (!acc->certStore().getCertificate(issuerUid)) {
     332             :                     // Check that parentCert
     333           1 :                     auto memberFile = fmt::format("{}members/{}.crt", git_repository_workdir(repo.get()), issuerUid);
     334           1 :                     auto adminFile = fmt::format("{}admins/{}.crt", git_repository_workdir(repo.get()), issuerUid);
     335           2 :                     auto parentCert = std::make_shared<dht::crypto::Certificate>(dhtnet::fileutils::loadFile(
     336           2 :                         std::filesystem::is_regular_file(memberFile, ec) ? memberFile : adminFile));
     337           0 :                     if (parentCert && (ignoreExpired || parentCert->getExpiration() < std::chrono::system_clock::now()))
     338           0 :                         acc->certStore().pinCertificate(parentCert,
     339             :                                                         true); // Pin certificate to local store if not already done
     340           2 :                 }
     341        1021 :                 if (!acc->certStore().getCertificate(cert->getPublicKey().getLongId().toString())) {
     342           0 :                     acc->certStore().pinCertificate(cert,
     343             :                                                     true); // Pin certificate to local store if not already done
     344             :                 }
     345        1021 :                 memberDevices[cert->getIssuerUID()].emplace_back(cert->getPublicKey().getLongId());
     346             : 
     347        1025 :             } catch (const std::exception&) {
     348           1 :             }
     349         513 :         }
     350         513 :         return memberDevices;
     351         513 :     }
     352             : 
     353       13829 :     bool hasCommit(const std::string& commitId) const
     354             :     {
     355       13829 :         auto repo = repository();
     356       13829 :         if (!repo)
     357           0 :             return false;
     358             : 
     359             :         git_oid oid;
     360       13830 :         if (git_oid_fromstr(&oid, commitId.c_str()) < 0)
     361           1 :             return false;
     362       13829 :         git_commit* commitPtr = nullptr;
     363       13829 :         if (git_commit_lookup(&commitPtr, repo.get(), &oid) < 0)
     364        3369 :             return false;
     365       10465 :         git_commit_free(commitPtr);
     366       10465 :         return true;
     367       13835 :     }
     368             : 
     369             :     ConversationCommit parseCommit(git_repository* repo, const git_commit* commit) const;
     370             : 
     371         977 :     std::optional<ConversationCommit> getCommit(const std::string& commitId) const
     372             :     {
     373         977 :         auto repo = repository();
     374         977 :         if (!repo)
     375           0 :             return std::nullopt;
     376             : 
     377             :         git_oid oid;
     378         977 :         if (git_oid_fromstr(&oid, commitId.c_str()) < 0)
     379           1 :             return std::nullopt;
     380             : 
     381         976 :         git_commit* commitPtr = nullptr;
     382         976 :         if (git_commit_lookup(&commitPtr, repo.get(), &oid) < 0)
     383           1 :             return std::nullopt;
     384         975 :         GitCommit commit {commitPtr};
     385             : 
     386         975 :         return parseCommit(repo.get(), commit.get());
     387         977 :     }
     388             : 
     389             :     bool resolveConflicts(git_index* index, const std::string& other_id);
     390             : 
     391        2981 :     std::set<std::string> memberUris(std::string_view filter, const std::set<MemberRole>& filteredRoles) const
     392             :     {
     393        2981 :         std::lock_guard lk(membersMtx_);
     394        2981 :         std::set<std::string> ret;
     395       34297 :         for (const auto& member : members_) {
     396       31326 :             if ((filteredRoles.find(member.role) != filteredRoles.end())
     397       31323 :                 or (not filter.empty() and filter == member.uri))
     398        1902 :                 continue;
     399       29419 :             ret.emplace(member.uri);
     400             :         }
     401        5962 :         return ret;
     402        2981 :     }
     403             : 
     404             :     void initMembers();
     405             : 
     406             :     std::optional<std::map<std::string, std::string>> convCommitToMap(const ConversationCommit& commit) const;
     407             : 
     408             :     // Permissions
     409             :     MemberRole updateProfilePermLvl_ {MemberRole::ADMIN};
     410             : 
     411             :     /**
     412             :      * Retrieve the user related to a device using the account's certificate store.
     413             :      * @note deviceToUri_ is used to cache result and avoid always loading the certificate
     414             :      */
     415       22850 :     std::string uriFromDevice(const std::string& deviceId, const std::string& commitId = "") const
     416             :     {
     417             :         // Check if we have the device in cache.
     418       22850 :         std::lock_guard lk(deviceToUriMtx_);
     419       22860 :         auto it = deviceToUri_.find(deviceId);
     420       22847 :         if (it != deviceToUri_.end())
     421       21491 :             return it->second;
     422             : 
     423        1364 :         auto acc = account_.lock();
     424        1364 :         if (!acc)
     425           0 :             return {};
     426             : 
     427        1364 :         auto cert = acc->certStore().getCertificate(deviceId);
     428        1364 :         if (!cert || !cert->issuer) {
     429           1 :             if (!commitId.empty()) {
     430           1 :                 std::string uri = uriFromDeviceAtCommit(deviceId, commitId);
     431           1 :                 if (!uri.empty()) {
     432           0 :                     deviceToUri_.insert({deviceId, uri});
     433           0 :                     return uri;
     434             :                 }
     435           1 :             }
     436             :             // Not pinned, so load certificate from repo
     437           1 :             auto repo = repository();
     438           1 :             if (!repo)
     439           0 :                 return {};
     440           2 :             auto deviceFile = std::filesystem::path(git_repository_workdir(repo.get())) / "devices"
     441           4 :                               / fmt::format("{}.crt", deviceId);
     442           1 :             if (!std::filesystem::is_regular_file(deviceFile))
     443           1 :                 return {};
     444             :             try {
     445           0 :                 cert = std::make_shared<dht::crypto::Certificate>(fileutils::loadFile(deviceFile));
     446           0 :             } catch (const std::exception&) {
     447           0 :                 JAMI_WARNING("Unable to load certificate from {}", deviceFile);
     448           0 :             }
     449           0 :             if (!cert)
     450           0 :                 return {};
     451           2 :         }
     452        1363 :         auto issuerUid = cert->issuer ? cert->issuer->getId().toString() : cert->getIssuerUID();
     453        1363 :         if (issuerUid.empty())
     454           0 :             return {};
     455             : 
     456        1363 :         deviceToUri_.insert({deviceId, issuerUid});
     457        1363 :         return issuerUid;
     458       22845 :     }
     459             :     mutable std::mutex deviceToUriMtx_;
     460             :     mutable std::map<std::string, std::string> deviceToUri_;
     461             : 
     462             :     /**
     463             :      * Retrieve the user related to a device using certificate directly from the repository at a
     464             :      * specific commit.
     465             :      * @note Prefer uriFromDevice() if possible as it uses the cache.
     466             :      */
     467           1 :     std::string uriFromDeviceAtCommit(const std::string& deviceId, const std::string& commitId) const
     468             :     {
     469           1 :         auto repo = repository();
     470           1 :         if (!repo)
     471           0 :             return {};
     472           1 :         auto tree = treeAtCommit(repo.get(), commitId);
     473           1 :         auto deviceFile = fmt::format("devices/{}.crt", deviceId);
     474           1 :         auto blob_device = fileAtTree(deviceFile, tree);
     475           1 :         if (!blob_device) {
     476           4 :             JAMI_ERROR("{} announced but not found", deviceId);
     477           1 :             return {};
     478             :         }
     479           0 :         auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
     480           0 :         return deviceCert.getIssuerUID();
     481           1 :     }
     482             : 
     483             :     /**
     484             :      * Verify that a certificate modification is correct
     485             :      * @param certPath      Where the certificate is saved (relative path)
     486             :      * @param userUri       Account we want for this certificate
     487             :      * @param oldCert       Previous certificate. getId() should return the same id as the new
     488             :      *                      certificate.
     489             :      * @note There is a few exception because JAMS certificates are buggy right now
     490             :      */
     491         380 :     bool verifyCertificate(std::string_view certContent,
     492             :                            const std::string& userUri,
     493             :                            std::string_view oldCert = ""sv) const
     494             :     {
     495         380 :         auto cert = dht::crypto::Certificate(certContent);
     496         380 :         auto isDeviceCertificate = cert.getId().toString() != userUri;
     497         380 :         auto issuerUid = cert.getIssuerUID();
     498         380 :         if (isDeviceCertificate && issuerUid.empty()) {
     499             :             // Err for Jams certificates
     500           0 :             JAMI_ERROR("Empty issuer for {}", cert.getId().toString());
     501             :         }
     502         380 :         if (!oldCert.empty()) {
     503           3 :             auto deviceCert = dht::crypto::Certificate(oldCert);
     504           3 :             if (isDeviceCertificate) {
     505           2 :                 if (issuerUid != deviceCert.getIssuerUID()) {
     506             :                     // NOTE: Here, because JAMS certificate can be incorrectly formatted, there is
     507             :                     // just one valid possibility: passing from an empty issuer to
     508             :                     // the valid issuer.
     509           1 :                     if (issuerUid != userUri) {
     510           4 :                         JAMI_ERROR("Device certificate with a bad issuer {}", cert.getId().toString());
     511           1 :                         return false;
     512             :                     }
     513             :                 }
     514           1 :             } else if (cert.getId().toString() != userUri) {
     515           0 :                 JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
     516           0 :                 return false;
     517             :             }
     518           2 :             if (cert.getId() != deviceCert.getId()) {
     519           0 :                 JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
     520           0 :                 return false;
     521             :             }
     522           2 :             return true;
     523           3 :         }
     524             : 
     525             :         // If it's a device certificate, we need to verify that the issuer is not modified
     526         377 :         if (isDeviceCertificate) {
     527             :             // Check that issuer is the one we want.
     528             :             // NOTE: Still one case due to incorrectly formatted certificates from JAMS
     529         190 :             if (issuerUid != userUri && !issuerUid.empty()) {
     530           4 :                 JAMI_ERROR("Device certificate with a bad issuer {}", cert.getId().toString());
     531           1 :                 return false;
     532             :             }
     533         187 :         } else if (cert.getId().toString() != userUri) {
     534           0 :             JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
     535           0 :             return false;
     536             :         }
     537             : 
     538         376 :         return true;
     539         380 :     }
     540             : 
     541             :     std::mutex opMtx_; // Mutex for operations
     542             : };
     543             : 
     544             : /////////////////////////////////////////////////////////////////////////////////
     545             : 
     546             : /**
     547             :  * Creates an empty repository
     548             :  * @param path       Path of the new repository
     549             :  * @return The libgit2's managed repository
     550             :  */
     551             : GitRepository
     552         199 : create_empty_repository(const std::string& path)
     553             : {
     554         199 :     git_repository* repo = nullptr;
     555             :     git_repository_init_options opts;
     556         199 :     git_repository_init_options_init(&opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION);
     557         199 :     opts.flags |= GIT_REPOSITORY_INIT_MKPATH;
     558         199 :     opts.initial_head = "main";
     559         199 :     if (git_repository_init_ext(&repo, path.c_str(), &opts) < 0) {
     560           0 :         JAMI_ERROR("Unable to create a git repository in {}", path);
     561             :     }
     562         398 :     return GitRepository(std::move(repo));
     563             : }
     564             : 
     565             : /**
     566             :  * Add all files to index
     567             :  * @param   repo
     568             :  * @return  if operation is successful
     569             :  */
     570             : bool
     571         377 : git_add_all(git_repository* repo)
     572             : {
     573             :     // git add -A
     574         377 :     git_index* index_ptr = nullptr;
     575         377 :     if (git_repository_index(&index_ptr, repo) < 0) {
     576           0 :         JAMI_ERROR("Unable to open repository index");
     577           0 :         return false;
     578             :     }
     579         377 :     GitIndex index {index_ptr};
     580         377 :     git_strarray array {nullptr, 0};
     581         377 :     git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
     582         377 :     git_index_write(index.get());
     583         377 :     git_strarray_dispose(&array);
     584         377 :     return true;
     585         377 : }
     586             : 
     587             : /**
     588             :  * Adds initial files. This adds the certificate of the account in the /admins directory
     589             :  * the device's key in /devices and the CRLs in /CRLs.
     590             :  * @param   repo      The repository
     591             :  * @return  if files were added successfully
     592             :  */
     593             : bool
     594         199 : add_initial_files(GitRepository& repo,
     595             :                   const std::shared_ptr<JamiAccount>& account,
     596             :                   ConversationMode mode,
     597             :                   const std::string& otherMember = "")
     598             : {
     599         199 :     auto deviceId = account->currentDeviceId();
     600         199 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
     601         199 :     auto adminsPath = repoPath / MemberPath::ADMINS;
     602         199 :     auto devicesPath = repoPath / MemberPath::DEVICES;
     603         199 :     auto invitedPath = repoPath / MemberPath::INVITED;
     604         398 :     auto crlsPath = repoPath / "CRLs" / deviceId;
     605             : 
     606         199 :     if (!dhtnet::fileutils::recursive_mkdir(adminsPath, 0700)) {
     607           0 :         JAMI_ERROR("Error when creating {}. Abort create conversations", adminsPath);
     608           0 :         return false;
     609             :     }
     610             : 
     611         199 :     auto cert = account->identity().second;
     612         199 :     auto deviceCert = cert->toString(false);
     613         199 :     auto parentCert = cert->issuer;
     614         199 :     if (!parentCert) {
     615           0 :         JAMI_ERROR("Parent cert is null");
     616           0 :         return false;
     617             :     }
     618             : 
     619             :     // /admins
     620         597 :     auto adminPath = adminsPath / fmt::format("{}.crt", parentCert->getId().toString());
     621         199 :     std::ofstream file(adminPath, std::ios::trunc | std::ios::binary);
     622         199 :     if (!file.is_open()) {
     623           0 :         JAMI_ERROR("Unable to write data to {}", adminPath);
     624           0 :         return false;
     625             :     }
     626         199 :     file << parentCert->toString(true);
     627         199 :     file.close();
     628             : 
     629         199 :     if (!dhtnet::fileutils::recursive_mkdir(devicesPath, 0700)) {
     630           0 :         JAMI_ERROR("Error when creating {}. Abort create conversations", devicesPath);
     631           0 :         return false;
     632             :     }
     633             : 
     634             :     // /devices
     635         597 :     auto devicePath = devicesPath / fmt::format("{}.crt", deviceId);
     636         199 :     file = std::ofstream(devicePath, std::ios::trunc | std::ios::binary);
     637         199 :     if (!file.is_open()) {
     638           0 :         JAMI_ERROR("Unable to write data to {}", devicePath);
     639           0 :         return false;
     640             :     }
     641         199 :     file << deviceCert;
     642         199 :     file.close();
     643             : 
     644         199 :     if (!dhtnet::fileutils::recursive_mkdir(crlsPath, 0700)) {
     645           0 :         JAMI_ERROR("Error when creating {}. Abort create conversations", crlsPath);
     646           0 :         return false;
     647             :     }
     648             : 
     649             :     // /CRLs
     650         199 :     for (const auto& crl : account->identity().second->getRevocationLists()) {
     651           0 :         if (!crl)
     652           0 :             continue;
     653           0 :         auto crlPath = crlsPath / deviceId / (dht::toHex(crl->getNumber()) + ".crl");
     654           0 :         std::ofstream file(crlPath, std::ios::trunc | std::ios::binary);
     655           0 :         if (!file.is_open()) {
     656           0 :             JAMI_ERROR("Unable to write data to {}", crlPath);
     657           0 :             return false;
     658             :         }
     659           0 :         file << crl->toString();
     660           0 :         file.close();
     661         199 :     }
     662             : 
     663             :     // /invited for one to one
     664         199 :     if (mode == ConversationMode::ONE_TO_ONE) {
     665          69 :         if (!dhtnet::fileutils::recursive_mkdir(invitedPath, 0700)) {
     666           0 :             JAMI_ERROR("Error when creating {}.", invitedPath);
     667           0 :             return false;
     668             :         }
     669          69 :         auto invitedMemberPath = invitedPath / otherMember;
     670          69 :         if (std::filesystem::is_regular_file(invitedMemberPath)) {
     671           0 :             JAMI_WARNING("Member {} already present", otherMember);
     672           0 :             return false;
     673             :         }
     674             : 
     675          69 :         std::ofstream file(invitedMemberPath, std::ios::trunc | std::ios::binary);
     676          69 :         if (!file.is_open()) {
     677           0 :             JAMI_ERROR("Unable to write data to {}", invitedMemberPath);
     678           0 :             return false;
     679             :         }
     680          69 :     }
     681             : 
     682         199 :     if (!git_add_all(repo.get())) {
     683           0 :         return false;
     684             :     }
     685             : 
     686         796 :     JAMI_LOG("Initial files added in {}", repoPath);
     687         199 :     return true;
     688         199 : }
     689             : 
     690             : /**
     691             :  * Sign and create the initial commit
     692             :  * @param repo          The Git repository
     693             :  * @param account       The account who signs
     694             :  * @param mode          The mode
     695             :  * @param otherMember   If one to one
     696             :  * @return              The first commit hash or empty if failed
     697             :  */
     698             : std::string
     699         199 : initial_commit(GitRepository& repo,
     700             :                const std::shared_ptr<JamiAccount>& account,
     701             :                ConversationMode mode,
     702             :                const std::string& otherMember = "")
     703             : {
     704         199 :     auto deviceId = std::string(account->currentDeviceId());
     705         199 :     auto name = account->getDisplayName();
     706         199 :     if (name.empty())
     707           0 :         name = deviceId;
     708         199 :     name = std::regex_replace(name, regex_display_name, "");
     709             : 
     710         199 :     git_signature* sig_ptr = nullptr;
     711         199 :     git_index* index_ptr = nullptr;
     712             :     git_oid tree_id, commit_id;
     713         199 :     git_tree* tree_ptr = nullptr;
     714             : 
     715             :     // Sign commit's buffer
     716         199 :     if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) {
     717           1 :         if (git_signature_new(&sig_ptr, deviceId.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) {
     718           0 :             JAMI_ERROR("Unable to create a commit signature.");
     719           0 :             return {};
     720             :         }
     721             :     }
     722         199 :     GitSignature sig {sig_ptr};
     723             : 
     724         199 :     if (git_repository_index(&index_ptr, repo.get()) < 0) {
     725           0 :         JAMI_ERROR("Unable to open the repository index");
     726           0 :         return {};
     727             :     }
     728         199 :     GitIndex index {index_ptr};
     729             : 
     730         199 :     if (git_index_write_tree(&tree_id, index.get()) < 0) {
     731           0 :         JAMI_ERROR("Unable to write initial tree from index");
     732           0 :         return {};
     733             :     }
     734             : 
     735         199 :     if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) {
     736           0 :         JAMI_ERROR("Unable to look up the initial tree");
     737           0 :         return {};
     738             :     }
     739         199 :     GitTree tree {tree_ptr};
     740             : 
     741         199 :     Json::Value json;
     742         199 :     json["mode"] = static_cast<int>(mode);
     743         199 :     if (mode == ConversationMode::ONE_TO_ONE) {
     744          69 :         json["invited"] = otherMember;
     745             :     }
     746         199 :     json["type"] = "initial";
     747             : 
     748         199 :     git_buf to_sign = {};
     749         398 :     if (git_commit_create_buffer(
     750         597 :             &to_sign, repo.get(), sig.get(), sig.get(), nullptr, json::toString(json).c_str(), tree.get(), 0, nullptr)
     751         199 :         < 0) {
     752           0 :         JAMI_ERROR("Unable to create initial buffer");
     753           0 :         return {};
     754             :     }
     755             : 
     756         199 :     std::string signed_str = base64::encode(account->identity().first->sign((const uint8_t*) to_sign.ptr, to_sign.size));
     757             : 
     758             :     // git commit -S
     759         199 :     if (git_commit_create_with_signature(&commit_id, repo.get(), to_sign.ptr, signed_str.c_str(), "signature") < 0) {
     760           0 :         git_buf_dispose(&to_sign);
     761           0 :         JAMI_ERROR("Unable to sign the initial commit");
     762           0 :         return {};
     763             :     }
     764         199 :     git_buf_dispose(&to_sign);
     765             : 
     766             :     // Move commit to main branch
     767         199 :     git_commit* commit = nullptr;
     768         199 :     if (git_commit_lookup(&commit, repo.get(), &commit_id) == 0) {
     769         199 :         git_reference* ref = nullptr;
     770         199 :         git_branch_create(&ref, repo.get(), "main", commit, true);
     771         199 :         git_commit_free(commit);
     772         199 :         git_reference_free(ref);
     773             :     }
     774             : 
     775         199 :     auto commit_str = git_oid_tostr_s(&commit_id);
     776         199 :     if (commit_str)
     777         199 :         return commit_str;
     778           0 :     return {};
     779         199 : }
     780             : 
     781             : //////////////////////////////////
     782             : 
     783             : GitSignature
     784         514 : ConversationRepository::Impl::signature()
     785             : {
     786         514 :     auto name = getDisplayName();
     787         514 :     if (name.empty()) {
     788           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to create a commit signature: no name set", accountId_, id_);
     789           0 :         return nullptr;
     790             :     }
     791             : 
     792         514 :     git_signature* sig_ptr = nullptr;
     793             :     // Sign commit's buffer
     794         514 :     if (git_signature_new(&sig_ptr, name.c_str(), deviceId_.c_str(), std::time(nullptr), 0) < 0) {
     795             :         // Maybe the display name is invalid (like " ") - try without
     796           1 :         int err = git_signature_new(&sig_ptr, deviceId_.c_str(), deviceId_.c_str(), std::time(nullptr), 0);
     797           1 :         if (err < 0) {
     798           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to create a commit signature: {}", accountId_, id_, err);
     799           0 :             return nullptr;
     800             :         }
     801             :     }
     802         514 :     return GitSignature(sig_ptr);
     803         514 : }
     804             : 
     805             : std::string
     806          20 : ConversationRepository::Impl::createMergeCommit(git_index* index, const std::string& wanted_ref)
     807             : {
     808          20 :     if (!validateDevice()) {
     809           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Invalid device. Not migrated?", accountId_, id_);
     810           0 :         return {};
     811             :     }
     812             :     // The merge will occur between current HEAD and wanted_ref
     813          20 :     git_reference* head_ref_ptr = nullptr;
     814          20 :     auto repo = repository();
     815          20 :     if (!repo || git_repository_head(&head_ref_ptr, repo.get()) < 0) {
     816           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get HEAD reference", accountId_, id_);
     817           0 :         return {};
     818             :     }
     819          20 :     GitReference head_ref {head_ref_ptr};
     820             : 
     821             :     // Maybe that's a ref, so DWIM it
     822          20 :     git_reference* merge_ref_ptr = nullptr;
     823          20 :     git_reference_dwim(&merge_ref_ptr, repo.get(), wanted_ref.c_str());
     824          20 :     GitReference merge_ref {merge_ref_ptr};
     825             : 
     826          20 :     GitSignature sig {signature()};
     827             : 
     828             :     // Prepare a standard merge commit message
     829          20 :     const char* msg_target = nullptr;
     830          20 :     if (merge_ref) {
     831           0 :         git_branch_name(&msg_target, merge_ref.get());
     832             :     } else {
     833          20 :         msg_target = wanted_ref.c_str();
     834             :     }
     835             : 
     836          20 :     auto commitMsg = fmt::format("Merge {} '{}'", merge_ref ? "branch" : "commit", msg_target);
     837             : 
     838             :     // Set up our parent commits
     839          60 :     GitCommit parents[2];
     840          20 :     git_commit* parent = nullptr;
     841          20 :     if (git_reference_peel((git_object**) &parent, head_ref.get(), GIT_OBJ_COMMIT) < 0) {
     842           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to peel HEAD reference", accountId_, id_);
     843           0 :         return {};
     844             :     }
     845          20 :     parents[0] = GitCommit(parent);
     846             :     git_oid commit_id;
     847          20 :     if (git_oid_fromstr(&commit_id, wanted_ref.c_str()) < 0) {
     848           0 :         return {};
     849             :     }
     850          20 :     git_annotated_commit* annotated_ptr = nullptr;
     851          20 :     if (git_annotated_commit_lookup(&annotated_ptr, repo.get(), &commit_id) < 0) {
     852           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up commit {}", accountId_, id_, wanted_ref);
     853           0 :         return {};
     854             :     }
     855          20 :     GitAnnotatedCommit annotated {annotated_ptr};
     856          20 :     if (git_commit_lookup(&parent, repo.get(), git_annotated_commit_id(annotated.get())) < 0) {
     857           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up commit {}", accountId_, id_, wanted_ref);
     858           0 :         return {};
     859             :     }
     860          20 :     parents[1] = GitCommit(parent);
     861             : 
     862             :     // Prepare our commit tree
     863             :     git_oid tree_oid;
     864          20 :     git_tree* tree_ptr = nullptr;
     865          20 :     if (git_index_write_tree_to(&tree_oid, index, repo.get()) < 0) {
     866           0 :         const git_error* err = giterr_last();
     867           0 :         if (err)
     868           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to write index: {}", accountId_, id_, err->message);
     869           0 :         return {};
     870             :     }
     871          20 :     if (git_tree_lookup(&tree_ptr, repo.get(), &tree_oid) < 0) {
     872           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up tree", accountId_, id_);
     873           0 :         return {};
     874             :     }
     875          20 :     GitTree tree {tree_ptr};
     876             : 
     877             :     // Commit
     878          20 :     git_buf to_sign = {};
     879             :     // The last argument of git_commit_create_buffer is of type
     880             :     // 'const git_commit **' in all versions of libgit2 except 1.8.0,
     881             :     // 1.8.1 and 1.8.3, in which it is of type 'git_commit *const *'.
     882             : #if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
     883             :     && (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
     884          20 :     git_commit* const parents_ptr[2] {parents[0].get(), parents[1].get()};
     885             : #else
     886             :     const git_commit* parents_ptr[2] {parents[0].get(), parents[1].get()};
     887             : #endif
     888          40 :     if (git_commit_create_buffer(
     889          40 :             &to_sign, repo.get(), sig.get(), sig.get(), nullptr, commitMsg.c_str(), tree.get(), 2, &parents_ptr[0])
     890          20 :         < 0) {
     891           0 :         const git_error* err = giterr_last();
     892           0 :         if (err)
     893           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to create commit buffer: {}",
     894             :                        accountId_,
     895             :                        id_,
     896             :                        err->message);
     897           0 :         return {};
     898             :     }
     899             : 
     900          20 :     auto account = account_.lock();
     901          20 :     if (!account)
     902           0 :         return {};
     903             :     // git commit -S
     904          20 :     auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
     905          20 :     auto signed_buf = account->identity().first->sign(to_sign_vec);
     906          20 :     std::string signed_str = base64::encode(signed_buf);
     907             :     git_oid commit_oid;
     908          20 :     if (git_commit_create_with_signature(&commit_oid, repo.get(), to_sign.ptr, signed_str.c_str(), "signature") < 0) {
     909           0 :         git_buf_dispose(&to_sign);
     910           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to sign commit", accountId_, id_);
     911           0 :         return {};
     912             :     }
     913          20 :     git_buf_dispose(&to_sign);
     914             : 
     915          20 :     auto commit_str = git_oid_tostr_s(&commit_oid);
     916          20 :     if (commit_str) {
     917          80 :         JAMI_LOG("[Account {}] [Conversation {}] New merge commit added with id: {}", accountId_, id_, commit_str);
     918             :         // Move commit to main branch
     919          20 :         git_reference* ref_ptr = nullptr;
     920          20 :         if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_oid, true, nullptr) < 0) {
     921           0 :             const git_error* err = giterr_last();
     922           0 :             if (err) {
     923           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] Unable to move commit to main: {}",
     924             :                            accountId_,
     925             :                            id_,
     926             :                            err->message);
     927           0 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, ECOMMIT, err->message);
     928             :             }
     929           0 :             return {};
     930             :         }
     931          20 :         git_reference_free(ref_ptr);
     932             :     }
     933             : 
     934             :     // We're done merging. Clean up the repository state and index
     935          20 :     git_repository_state_cleanup(repo.get());
     936             : 
     937          20 :     git_object* target_ptr = nullptr;
     938          20 :     if (git_object_lookup(&target_ptr, repo.get(), &commit_oid, GIT_OBJ_COMMIT) != 0) {
     939           0 :         const git_error* err = giterr_last();
     940           0 :         if (err)
     941           0 :             JAMI_ERROR("[Account {}] [Conversation {}] failed to look up OID {}: {}",
     942             :                        accountId_,
     943             :                        id_,
     944             :                        git_oid_tostr_s(&commit_oid),
     945             :                        err->message);
     946           0 :         return {};
     947             :     }
     948          20 :     GitObject target {target_ptr};
     949             : 
     950          20 :     git_reset(repo.get(), target.get(), GIT_RESET_HARD, nullptr);
     951             : 
     952          20 :     return commit_str ? commit_str : "";
     953         100 : }
     954             : 
     955             : bool
     956         892 : ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is_unborn)
     957             : {
     958             :     // Initialize target
     959         892 :     git_reference* target_ref_ptr = nullptr;
     960         892 :     auto repo = repository();
     961         892 :     if (!repo) {
     962           0 :         JAMI_ERROR("[Account {}] [Conversation {}] No repository found", accountId_, id_);
     963           0 :         return false;
     964             :     }
     965         892 :     if (is_unborn) {
     966           0 :         git_reference* head_ref_ptr = nullptr;
     967             :         // HEAD reference is unborn, lookup manually so we don't try to resolve it
     968           0 :         if (git_reference_lookup(&head_ref_ptr, repo.get(), "HEAD") < 0) {
     969           0 :             JAMI_ERROR("[Account {}] [Conversation {}] failed to look up HEAD ref", accountId_, id_);
     970           0 :             return false;
     971             :         }
     972           0 :         GitReference head_ref {head_ref_ptr};
     973             : 
     974             :         // Grab the reference HEAD should be pointing to
     975           0 :         const auto* symbolic_ref = git_reference_symbolic_target(head_ref.get());
     976             : 
     977             :         // Create our main reference on the target OID
     978           0 :         if (git_reference_create(&target_ref_ptr, repo.get(), symbolic_ref, target_oid, 0, nullptr) < 0) {
     979           0 :             const git_error* err = giterr_last();
     980           0 :             if (err)
     981           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] failed to create main reference: {}",
     982             :                            accountId_,
     983             :                            id_,
     984             :                            err->message);
     985           0 :             return false;
     986             :         }
     987             : 
     988         892 :     } else if (git_repository_head(&target_ref_ptr, repo.get()) < 0) {
     989             :         // HEAD exists, just look up and resolve
     990           0 :         JAMI_ERROR("[Account {}] [Conversation {}] failed to get HEAD reference", accountId_, id_);
     991           0 :         return false;
     992             :     }
     993         892 :     GitReference target_ref {target_ref_ptr};
     994             : 
     995             :     // Look up the target object
     996         892 :     git_object* target_ptr = nullptr;
     997         892 :     if (git_object_lookup(&target_ptr, repo.get(), target_oid, GIT_OBJ_COMMIT) != 0) {
     998           0 :         JAMI_ERROR("[Account {}] [Conversation {}] failed to look up OID {}",
     999             :                    accountId_,
    1000             :                    id_,
    1001             :                    git_oid_tostr_s(target_oid));
    1002           0 :         return false;
    1003             :     }
    1004         892 :     GitObject target {target_ptr};
    1005             : 
    1006             :     // Checkout the result so the workdir is in the expected state
    1007             :     git_checkout_options ff_checkout_options;
    1008         892 :     git_checkout_init_options(&ff_checkout_options, GIT_CHECKOUT_OPTIONS_VERSION);
    1009         892 :     ff_checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE;
    1010         892 :     if (git_checkout_tree(repo.get(), target.get(), &ff_checkout_options) != 0) {
    1011           0 :         if (auto err = git_error_last())
    1012           0 :             JAMI_ERROR("[Account {}] [Conversation {}] failed to checkout HEAD reference: {}",
    1013             :                        accountId_,
    1014             :                        id_,
    1015             :                        err->message);
    1016             :         else
    1017           0 :             JAMI_ERROR("[Account {}] [Conversation {}] failed to checkout HEAD reference: unknown error",
    1018             :                        accountId_,
    1019             :                        id_);
    1020           0 :         return false;
    1021             :     }
    1022             : 
    1023             :     // Move the target reference to the target OID
    1024             :     git_reference* new_target_ref;
    1025         892 :     if (git_reference_set_target(&new_target_ref, target_ref.get(), target_oid, nullptr) < 0) {
    1026           0 :         JAMI_ERROR("[Account {}] [Conversation {}] failed to move HEAD reference", accountId_, id_);
    1027           0 :         return false;
    1028             :     }
    1029         892 :     git_reference_free(new_target_ref);
    1030             : 
    1031         892 :     return true;
    1032         892 : }
    1033             : 
    1034             : bool
    1035         322 : ConversationRepository::Impl::add(const std::string& path)
    1036             : {
    1037         322 :     auto repo = repository();
    1038         322 :     if (!repo)
    1039           0 :         return false;
    1040         322 :     git_index* index_ptr = nullptr;
    1041         322 :     if (git_repository_index(&index_ptr, repo.get()) < 0) {
    1042           0 :         JAMI_ERROR("Unable to open repository index");
    1043           0 :         return false;
    1044             :     }
    1045         322 :     GitIndex index {index_ptr};
    1046         322 :     if (git_index_add_bypath(index.get(), path.c_str()) != 0) {
    1047           0 :         const git_error* err = giterr_last();
    1048           0 :         if (err)
    1049           0 :             JAMI_ERROR("Error when adding file: {}", err->message);
    1050           0 :         return false;
    1051             :     }
    1052         322 :     return git_index_write(index.get()) == 0;
    1053         322 : }
    1054             : 
    1055             : bool
    1056         159 : ConversationRepository::Impl::checkValidUserDiff(const std::string& userDevice,
    1057             :                                                  const std::string& commitId,
    1058             :                                                  const std::string& parentId) const
    1059             : {
    1060             :     // Retrieve tree for recent commit
    1061         159 :     auto repo = repository();
    1062         159 :     if (!repo)
    1063           0 :         return false;
    1064             :     // Here, we check that a file device is modified or not.
    1065         159 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1066         159 :     if (changedFiles.size() == 0)
    1067         151 :         return true;
    1068             : 
    1069             :     // If a certificate is modified (in the changedFiles), it MUST be a certificate from the user
    1070             :     // Retrieve userUri
    1071           8 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1072           8 :     auto userUri = uriFromDevice(userDevice, commitId);
    1073           8 :     if (userUri.empty())
    1074           0 :         return false;
    1075             : 
    1076           8 :     std::string userDeviceFile = fmt::format("devices/{}.crt", userDevice);
    1077           8 :     std::string adminsFile = fmt::format("admins/{}.crt", userUri);
    1078           8 :     std::string membersFile = fmt::format("members/{}.crt", userUri);
    1079           8 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1080           8 :     if (not treeNew or not treeOld)
    1081           0 :         return false;
    1082          12 :     for (const auto& changedFile : changedFiles) {
    1083           9 :         if (changedFile == adminsFile || changedFile == membersFile) {
    1084             :             // In this case, we should verify it's not added (normal commit, not a member change)
    1085             :             // but only updated
    1086           1 :             auto oldFile = fileAtTree(changedFile, treeOld);
    1087           1 :             if (!oldFile) {
    1088           0 :                 JAMI_ERROR("Invalid file modified: {}", changedFile);
    1089           0 :                 return false;
    1090             :             }
    1091           1 :             auto newFile = fileAtTree(changedFile, treeNew);
    1092           1 :             if (!verifyCertificate(as_view(newFile), userUri, as_view(oldFile))) {
    1093           0 :                 JAMI_ERROR("Invalid certificate {}", changedFile);
    1094           0 :                 return false;
    1095             :             }
    1096           9 :         } else if (changedFile == userDeviceFile) {
    1097             :             // In this case, device is added or modified (certificate expiration)
    1098           4 :             auto oldFile = fileAtTree(changedFile, treeOld);
    1099           4 :             std::string_view oldCert;
    1100           4 :             if (oldFile)
    1101           2 :                 oldCert = as_view(oldFile);
    1102           4 :             auto newFile = fileAtTree(changedFile, treeNew);
    1103           4 :             if (!verifyCertificate(as_view(newFile), userUri, oldCert)) {
    1104           4 :                 JAMI_ERROR("Invalid certificate {}", changedFile);
    1105           1 :                 return false;
    1106             :             }
    1107           5 :         } else {
    1108             :             // Invalid file detected
    1109          16 :             JAMI_ERROR("Invalid add file detected: {} {}", changedFile, (int) mode());
    1110           4 :             return false;
    1111             :         }
    1112             :     }
    1113             : 
    1114           3 :     return true;
    1115         159 : }
    1116             : 
    1117             : bool
    1118           2 : ConversationRepository::Impl::checkEdit(const std::string& userDevice, const ConversationCommit& commit) const
    1119             : {
    1120           2 :     auto repo = repository();
    1121           2 :     if (!repo)
    1122           0 :         return false;
    1123           2 :     auto userUri = uriFromDevice(userDevice, commit.id);
    1124           2 :     if (userUri.empty())
    1125           0 :         return false;
    1126             :     // Check that edited commit is found, for the same author, and editable (plain/text)
    1127           2 :     auto commitMap = convCommitToMap(commit);
    1128           2 :     if (commitMap == std::nullopt) {
    1129           0 :         return false;
    1130             :     }
    1131           4 :     auto editedId = commitMap->at("edit");
    1132           2 :     auto editedCommit = getCommit(editedId);
    1133           2 :     if (editedCommit == std::nullopt) {
    1134           0 :         JAMI_ERROR("Commit {:s} not found", editedId);
    1135           0 :         return false;
    1136             :     }
    1137           2 :     auto editedCommitMap = convCommitToMap(*editedCommit);
    1138           4 :     if (editedCommitMap == std::nullopt or editedCommitMap->at("author").empty()
    1139           4 :         or editedCommitMap->at("author") != commitMap->at("author") or commitMap->at("author") != userUri) {
    1140           0 :         JAMI_ERROR("Edited commit {:s} got a different author ({:s})", editedId, commit.id);
    1141           0 :         return false;
    1142             :     }
    1143           2 :     if (editedCommitMap->at("type") == "text/plain") {
    1144           1 :         return true;
    1145             :     }
    1146           1 :     if (editedCommitMap->at("type") == "application/data-transfer+json") {
    1147           0 :         if (editedCommitMap->find("tid") != editedCommitMap->end())
    1148           0 :             return true;
    1149             :     }
    1150           4 :     JAMI_ERROR("Edited commit {:s} is not valid!", editedId);
    1151           1 :     return false;
    1152           2 : }
    1153             : 
    1154             : bool
    1155           8 : ConversationRepository::Impl::checkVote(const std::string& userDevice,
    1156             :                                         const std::string& commitId,
    1157             :                                         const std::string& parentId) const
    1158             : {
    1159             :     // Check that maximum deviceFile and a vote is added
    1160           8 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1161           8 :     if (changedFiles.size() == 0) {
    1162           2 :         return true;
    1163           6 :     } else if (changedFiles.size() > 2) {
    1164           0 :         return false;
    1165             :     }
    1166             :     // If modified, it's the first commit of a device, we check
    1167             :     // that the file wasn't there previously. And the vote MUST be added
    1168           6 :     std::string deviceFile = "";
    1169           6 :     std::string votedFile = "";
    1170          13 :     for (const auto& changedFile : changedFiles) {
    1171             :         // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1172          16 :         if (changedFile == fmt::format("devices/{}.crt", userDevice)) {
    1173           2 :             deviceFile = changedFile;
    1174           6 :         } else if (changedFile.find("votes") == 0) {
    1175           5 :             votedFile = changedFile;
    1176             :         } else {
    1177             :             // Invalid file detected
    1178           4 :             JAMI_ERROR("Invalid vote file detected: {}", changedFile);
    1179           1 :             return false;
    1180             :         }
    1181             :     }
    1182             : 
    1183           5 :     if (votedFile.empty()) {
    1184           0 :         JAMI_WARNING("No vote detected for commit {}", commitId);
    1185           0 :         return false;
    1186             :     }
    1187             : 
    1188           5 :     auto repo = repository();
    1189           5 :     if (!repo)
    1190           0 :         return false;
    1191           5 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1192           5 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1193           5 :     if (not treeNew or not treeOld)
    1194           0 :         return false;
    1195             : 
    1196           5 :     auto userUri = uriFromDevice(userDevice, commitId);
    1197           5 :     if (userUri.empty())
    1198           0 :         return false;
    1199             :     // Check that voter is admin
    1200           5 :     auto adminFile = fmt::format("admins/{}.crt", userUri);
    1201             : 
    1202           5 :     if (!fileAtTree(adminFile, treeOld)) {
    1203           0 :         JAMI_ERROR("Vote from non admin: {}", userUri);
    1204           0 :         return false;
    1205             :     }
    1206             : 
    1207             :     // Check votedFile path
    1208           5 :     static const std::regex regex_votes("votes.(\\w+).(members|devices|admins|invited).(\\w+).(\\w+)");
    1209           5 :     std::svmatch base_match;
    1210           5 :     if (!std::regex_match(votedFile, base_match, regex_votes) or base_match.size() != 5) {
    1211           0 :         JAMI_WARNING("Invalid votes path: {}", votedFile);
    1212           0 :         return false;
    1213             :     }
    1214             : 
    1215           5 :     std::string_view matchedUri = svsub_match_view(base_match[4]);
    1216           5 :     if (matchedUri != userUri) {
    1217           0 :         JAMI_ERROR("Admin voted for other user: {:s} vs {:s}", userUri, matchedUri);
    1218           0 :         return false;
    1219             :     }
    1220           5 :     std::string_view votedUri = svsub_match_view(base_match[3]);
    1221           5 :     std::string_view type = svsub_match_view(base_match[2]);
    1222           5 :     std::string_view voteType = svsub_match_view(base_match[1]);
    1223           5 :     if (voteType != "ban" && voteType != "unban") {
    1224           0 :         JAMI_ERROR("Unrecognized vote {:s}", voteType);
    1225           0 :         return false;
    1226             :     }
    1227             : 
    1228             :     // Check that vote file is empty and wasn't modified
    1229           5 :     if (fileAtTree(votedFile, treeOld)) {
    1230           0 :         JAMI_ERROR("Invalid voted file modified: {:s}", votedFile);
    1231           0 :         return false;
    1232             :     }
    1233           5 :     auto vote = fileAtTree(votedFile, treeNew);
    1234           5 :     if (!vote) {
    1235           0 :         JAMI_ERROR("No vote file found for: {:s}", userUri);
    1236           0 :         return false;
    1237             :     }
    1238           5 :     auto voteContent = as_view(vote);
    1239           5 :     if (!voteContent.empty()) {
    1240           0 :         JAMI_ERROR("Vote file not empty: {:s}", votedFile);
    1241           0 :         return false;
    1242             :     }
    1243             : 
    1244             :     // Check that peer voted is only other device or other member
    1245           5 :     if (type != "devices") {
    1246           5 :         if (votedUri == userUri) {
    1247           0 :             JAMI_ERROR("Detected vote for self: {:s}", votedUri);
    1248           0 :             return false;
    1249             :         }
    1250           5 :         if (voteType == "ban") {
    1251             :             // file in members or admin or invited
    1252           5 :             auto invitedFile = fmt::format("invited/{}", votedUri);
    1253           5 :             if (!memberCertificate(votedUri, treeOld) && !fileAtTree(invitedFile, treeOld)) {
    1254           0 :                 JAMI_ERROR("No member file found for vote: {:s}", votedUri);
    1255           0 :                 return false;
    1256             :             }
    1257           5 :         }
    1258             :     } else {
    1259             :         // Check not current device
    1260           0 :         if (votedUri == userDevice) {
    1261           0 :             JAMI_ERROR("Detected vote for self: {:s}", votedUri);
    1262           0 :             return false;
    1263             :         }
    1264             :         // File in devices
    1265           0 :         deviceFile = fmt::format("devices/{}.crt", votedUri);
    1266           0 :         if (!fileAtTree(deviceFile, treeOld)) {
    1267           0 :             JAMI_ERROR("No device file found for vote: {:s}", votedUri);
    1268           0 :             return false;
    1269             :         }
    1270             :     }
    1271             : 
    1272           5 :     return true;
    1273           8 : }
    1274             : 
    1275             : bool
    1276         756 : ConversationRepository::Impl::checkValidAdd(const std::string& userDevice,
    1277             :                                             const std::string& uriMember,
    1278             :                                             const std::string& commitId,
    1279             :                                             const std::string& parentId) const
    1280             : {
    1281         756 :     auto repo = repository();
    1282         756 :     if (not repo)
    1283           0 :         return false;
    1284             : 
    1285             :     // std::string repoPath = git_repository_workdir(repo.get());
    1286         756 :     if (mode() == ConversationMode::ONE_TO_ONE) {
    1287           1 :         auto initialMembers = getInitialMembers();
    1288           1 :         auto it = std::find(initialMembers.begin(), initialMembers.end(), uriMember);
    1289           1 :         if (it == initialMembers.end()) {
    1290           4 :             JAMI_ERROR("Invalid add in one to one conversation: {}", uriMember);
    1291           1 :             return false;
    1292             :         }
    1293           1 :     }
    1294             : 
    1295         755 :     auto userUri = uriFromDevice(userDevice, commitId);
    1296         755 :     if (userUri.empty())
    1297           0 :         return false;
    1298             : 
    1299             :     // Check that only /invited/uri.crt is added & deviceFile & CRLs
    1300         755 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1301         755 :     if (changedFiles.size() == 0) {
    1302           0 :         return false;
    1303         755 :     } else if (changedFiles.size() > 3) {
    1304           0 :         return false;
    1305             :     }
    1306             : 
    1307             :     // Check that user added is not sender
    1308         755 :     if (userUri == uriMember) {
    1309           0 :         JAMI_ERROR("Member tried to add self: {}", userUri);
    1310           0 :         return false;
    1311             :     }
    1312             : 
    1313             :     // If modified, it's the first commit of a device, we check
    1314             :     // that the file wasn't there previously. And the member MUST be added
    1315             :     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1316         755 :     std::string deviceFile = "";
    1317         755 :     std::string invitedFile = "";
    1318        1510 :     std::string crlFile = std::string("CRLs/") + userUri;
    1319        1509 :     for (const auto& changedFile : changedFiles) {
    1320         756 :         if (changedFile == std::string("devices/") + userDevice + ".crt") {
    1321           1 :             deviceFile = changedFile;
    1322         755 :         } else if (changedFile == std::string("invited/") + uriMember) {
    1323         753 :             invitedFile = changedFile;
    1324           2 :         } else if (changedFile == crlFile) {
    1325             :             // Nothing to do
    1326             :         } else {
    1327             :             // Invalid file detected
    1328           8 :             JAMI_ERROR("Invalid add file detected: {}", changedFile);
    1329           2 :             return false;
    1330             :         }
    1331             :     }
    1332             : 
    1333         753 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1334         753 :     if (not treeOld)
    1335           0 :         return false;
    1336             : 
    1337         753 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1338         753 :     auto blob_invite = fileAtTree(invitedFile, treeNew);
    1339         753 :     if (!blob_invite) {
    1340           0 :         JAMI_ERROR("Invitation not found for commit {}", commitId);
    1341           0 :         return false;
    1342             :     }
    1343             : 
    1344         753 :     auto invitation = as_view(blob_invite);
    1345         752 :     if (!invitation.empty()) {
    1346           0 :         JAMI_ERROR("Invitation not empty for commit {}", commitId);
    1347           0 :         return false;
    1348             :     }
    1349             : 
    1350             :     // Check that user not in /banned
    1351             :     std::string bannedFile = fmt::format("{}/{}/{}.crt",
    1352        1505 :                                          MemberPath::BANNED.string(),
    1353         752 :                                          MemberPath::MEMBERS.string(),
    1354        1505 :                                          uriMember);
    1355         753 :     if (fileAtTree(bannedFile, treeOld)) {
    1356           0 :         JAMI_ERROR("Tried to add banned member: {}", bannedFile);
    1357           0 :         return false;
    1358             :     }
    1359             : 
    1360         753 :     return true;
    1361         756 : }
    1362             : 
    1363             : bool
    1364         790 : ConversationRepository::Impl::checkValidJoins(const std::string& userDevice,
    1365             :                                               const std::string& uriMember,
    1366             :                                               const std::string& commitId,
    1367             :                                               const std::string& parentId) const
    1368             : {
    1369             :     // Check no other files changed
    1370         790 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1371         790 :     auto invitedFile = fmt::format("invited/{}", uriMember);
    1372         790 :     auto membersFile = fmt::format("members/{}.crt", uriMember);
    1373         790 :     auto deviceFile = fmt::format("devices/{}.crt", userDevice);
    1374             : 
    1375        3154 :     for (auto& file : changedFiles) {
    1376        2364 :         if (file != invitedFile && file != membersFile && file != deviceFile) {
    1377           0 :             JAMI_ERROR("Unwanted file {} found", file);
    1378           0 :             return false;
    1379             :         }
    1380             :     }
    1381             : 
    1382             :     // Retrieve tree for commits
    1383         790 :     auto repo = repository();
    1384         790 :     assert(repo);
    1385         790 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1386         790 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1387         790 :     if (not treeNew or not treeOld)
    1388           0 :         return false;
    1389             : 
    1390             :     // Check /invited
    1391         790 :     if (fileAtTree(invitedFile, treeNew)) {
    1392           8 :         JAMI_ERROR("{} invited not removed", uriMember);
    1393           2 :         return false;
    1394             :     }
    1395         788 :     if (!fileAtTree(invitedFile, treeOld)) {
    1396           4 :         JAMI_ERROR("{} invited not found", uriMember);
    1397           1 :         return false;
    1398             :     }
    1399             : 
    1400             :     // Check /members added
    1401         787 :     if (!fileAtTree(membersFile, treeNew)) {
    1402           0 :         JAMI_ERROR("{} members not found", uriMember);
    1403           0 :         return false;
    1404             :     }
    1405         787 :     if (fileAtTree(membersFile, treeOld)) {
    1406           0 :         JAMI_ERROR("{} members found too soon", uriMember);
    1407           0 :         return false;
    1408             :     }
    1409             : 
    1410             :     // Check /devices added
    1411         787 :     if (!fileAtTree(deviceFile, treeNew)) {
    1412           0 :         JAMI_ERROR("{} devices not found", uriMember);
    1413           0 :         return false;
    1414             :     }
    1415             : 
    1416             :     // Check certificate
    1417         787 :     auto blob_device = fileAtTree(deviceFile, treeNew);
    1418         787 :     if (!blob_device) {
    1419           0 :         JAMI_ERROR("{} announced but not found", deviceFile);
    1420           0 :         return false;
    1421             :     }
    1422         787 :     auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
    1423         787 :     auto blob_member = fileAtTree(membersFile, treeNew);
    1424         787 :     if (!blob_member) {
    1425           0 :         JAMI_ERROR("{} announced but not found", userDevice);
    1426           0 :         return false;
    1427             :     }
    1428         787 :     auto memberCert = dht::crypto::Certificate(as_view(blob_member));
    1429         787 :     if (memberCert.getId().toString() != deviceCert.getIssuerUID() || deviceCert.getIssuerUID() != uriMember) {
    1430           0 :         JAMI_ERROR("Incorrect device certificate {} for user {}", userDevice, uriMember);
    1431           0 :         return false;
    1432             :     }
    1433             : 
    1434         787 :     return true;
    1435         790 : }
    1436             : 
    1437             : bool
    1438           1 : ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
    1439             :                                                const std::string& uriMember,
    1440             :                                                const std::string& commitId,
    1441             :                                                const std::string& parentId) const
    1442             : {
    1443             :     // Retrieve tree for recent commit
    1444           1 :     auto repo = repository();
    1445           1 :     if (!repo)
    1446           0 :         return false;
    1447           1 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1448           1 :     if (not treeOld)
    1449           0 :         return false;
    1450             : 
    1451           1 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1452             :     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1453           1 :     std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
    1454           1 :     std::string adminFile = fmt::format("admins/{}.crt", uriMember);
    1455           1 :     std::string memberFile = fmt::format("members/{}.crt", uriMember);
    1456           1 :     std::string crlFile = fmt::format("CRLs/{}", uriMember);
    1457           1 :     std::string invitedFile = fmt::format("invited/{}", uriMember);
    1458           1 :     std::vector<std::string> devicesRemoved;
    1459             : 
    1460             :     // Check that no weird file is added nor removed
    1461           1 :     static const std::regex regex_devices("devices.(\\w+)\\.crt");
    1462           1 :     std::smatch base_match;
    1463           3 :     for (const auto& f : changedFiles) {
    1464           2 :         if (f == deviceFile || f == adminFile || f == memberFile || f == crlFile || f == invitedFile) {
    1465             :             // Ignore
    1466           2 :             continue;
    1467           0 :         } else if (std::regex_match(f, base_match, regex_devices)) {
    1468           0 :             if (base_match.size() == 2)
    1469           0 :                 devicesRemoved.emplace_back(base_match[1]);
    1470             :         } else {
    1471           0 :             JAMI_ERROR("Unwanted changed file detected: {}", f);
    1472           0 :             return false;
    1473             :         }
    1474             :     }
    1475             : 
    1476             :     // Check that removed devices are for removed member (or directly uriMember)
    1477           1 :     for (const auto& deviceUri : devicesRemoved) {
    1478           0 :         deviceFile = fmt::format("devices/{}.crt", deviceUri);
    1479           0 :         auto blob_device = fileAtTree(deviceFile, treeOld);
    1480           0 :         if (!blob_device) {
    1481           0 :             JAMI_ERROR("Device not found added ({})", deviceFile);
    1482           0 :             return false;
    1483             :         }
    1484           0 :         auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
    1485           0 :         auto userUri = deviceCert.getIssuerUID();
    1486             : 
    1487           0 :         if (uriMember != userUri and uriMember != deviceUri /* If device is removed */) {
    1488           0 :             JAMI_ERROR("Device removed but not for removed user ({})", deviceFile);
    1489           0 :             return false;
    1490             :         }
    1491           0 :     }
    1492             : 
    1493           1 :     return true;
    1494           1 : }
    1495             : 
    1496             : bool
    1497           9 : ConversationRepository::Impl::checkValidVoteResolution(const std::string& userDevice,
    1498             :                                                        const std::string& uriMember,
    1499             :                                                        const std::string& commitId,
    1500             :                                                        const std::string& parentId,
    1501             :                                                        const std::string& voteType) const
    1502             : {
    1503             :     // Retrieve tree for recent commit
    1504           9 :     auto repo = repository();
    1505           9 :     if (!repo)
    1506           0 :         return false;
    1507           9 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1508           9 :     if (not treeOld)
    1509           0 :         return false;
    1510             : 
    1511           9 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1512             :     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1513           9 :     std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
    1514           9 :     std::string adminFile = fmt::format("admins/{}.crt", uriMember);
    1515           9 :     std::string memberFile = fmt::format("members/{}.crt", uriMember);
    1516           9 :     std::string crlFile = fmt::format("CRLs/{}", uriMember);
    1517           9 :     std::string invitedFile = fmt::format("invited/{}", uriMember);
    1518           9 :     std::vector<std::string> voters;
    1519           9 :     std::vector<std::string> devicesRemoved;
    1520           9 :     std::vector<std::string> bannedFiles;
    1521             :     // Check that no weird file is added nor removed
    1522             : 
    1523          18 :     const std::regex regex_votes("votes." + voteType + ".(members|devices|admins|invited).(\\w+).(\\w+)");
    1524           9 :     static const std::regex regex_devices("devices.(\\w+)\\.crt");
    1525           9 :     static const std::regex regex_banned("banned.(members|devices|admins).(\\w+)\\.crt");
    1526           9 :     static const std::regex regex_banned_invited("banned.(invited).(\\w+)");
    1527           9 :     std::smatch base_match;
    1528          24 :     for (const auto& f : changedFiles) {
    1529          17 :         if (f == deviceFile || f == adminFile || f == memberFile || f == crlFile || f == invitedFile) {
    1530             :             // Ignore
    1531           5 :             continue;
    1532          12 :         } else if (std::regex_match(f, base_match, regex_votes)) {
    1533           5 :             if (base_match.size() != 4 or base_match[2] != uriMember) {
    1534           0 :                 JAMI_ERROR("Invalid vote file detected: {}", f);
    1535           2 :                 return false;
    1536             :             }
    1537           5 :             voters.emplace_back(base_match[3]);
    1538             :             // Check that votes were not added here
    1539           5 :             if (!fileAtTree(f, treeOld)) {
    1540           0 :                 JAMI_ERROR("invalid vote added ({})", f);
    1541           0 :                 return false;
    1542             :             }
    1543           7 :         } else if (std::regex_match(f, base_match, regex_devices)) {
    1544           0 :             if (base_match.size() == 2)
    1545           0 :                 devicesRemoved.emplace_back(base_match[1]);
    1546           7 :         } else if (std::regex_match(f, base_match, regex_banned)
    1547           7 :                    || std::regex_match(f, base_match, regex_banned_invited)) {
    1548           5 :             bannedFiles.emplace_back(f);
    1549           5 :             if (base_match.size() != 3 or base_match[2] != uriMember) {
    1550           0 :                 JAMI_ERROR("Invalid banned file detected : {}", f);
    1551           0 :                 return false;
    1552             :             }
    1553             :         } else {
    1554           8 :             JAMI_ERROR("Unwanted changed file detected: {}", f);
    1555           2 :             return false;
    1556             :         }
    1557             :     }
    1558             : 
    1559             :     // Check that removed devices are for removed member (or directly uriMember)
    1560           7 :     for (const auto& deviceUri : devicesRemoved) {
    1561           0 :         deviceFile = fmt::format("devices/{}.crt", deviceUri);
    1562           0 :         if (voteType == "ban") {
    1563             :             // If we ban a device, it should be there before
    1564           0 :             if (!fileAtTree(deviceFile, treeOld)) {
    1565           0 :                 JAMI_ERROR("Device not found added ({})", deviceFile);
    1566           0 :                 return false;
    1567             :             }
    1568           0 :         } else if (voteType == "unban") {
    1569             :             // If we unban a device, it should not be there before
    1570           0 :             if (fileAtTree(deviceFile, treeOld)) {
    1571           0 :                 JAMI_ERROR("Device not found added ({})", deviceFile);
    1572           0 :                 return false;
    1573             :             }
    1574             :         }
    1575           0 :         if (uriMember != uriFromDevice(deviceUri) and uriMember != deviceUri /* If device is removed */) {
    1576           0 :             JAMI_ERROR("Device removed but not for removed user ({})", deviceFile);
    1577           0 :             return false;
    1578             :         }
    1579             :     }
    1580             : 
    1581           7 :     auto userUri = uriFromDevice(userDevice, commitId);
    1582           7 :     if (userUri.empty())
    1583           0 :         return false;
    1584             : 
    1585             :     // Check that voters are admins
    1586          14 :     adminFile = fmt::format("admins/{}.crt", userUri);
    1587           7 :     if (!fileAtTree(adminFile, treeOld)) {
    1588           4 :         JAMI_ERROR("admin file ({}) not found", adminFile);
    1589           1 :         return false;
    1590             :     }
    1591             : 
    1592             :     // If not for self check that vote is valid and not added
    1593           6 :     auto nbAdmins = 0;
    1594           6 :     auto nbVotes = 0;
    1595           6 :     std::string repoPath = git_repository_workdir(repo.get());
    1596          12 :     for (const auto& certificate : dhtnet::fileutils::readDirectory(repoPath + "admins")) {
    1597           6 :         if (certificate.find(".crt") == std::string::npos) {
    1598           0 :             JAMI_WARNING("Incorrect file found: {}", certificate);
    1599           0 :             continue;
    1600           0 :         }
    1601           6 :         nbAdmins += 1;
    1602          12 :         auto adminUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
    1603           6 :         if (std::find(voters.begin(), voters.end(), adminUri) != voters.end()) {
    1604           5 :             nbVotes += 1;
    1605             :         }
    1606          12 :     }
    1607             : 
    1608           6 :     if (nbAdmins == 0 or (static_cast<double>(nbVotes) / static_cast<double>(nbAdmins)) < .5) {
    1609           4 :         JAMI_ERROR("Incomplete vote detected (commit: {})", commitId);
    1610           1 :         return false;
    1611             :     }
    1612             : 
    1613             :     // If not for self check that member or device certificate is moved to banned/
    1614           5 :     return !bannedFiles.empty();
    1615           9 : }
    1616             : 
    1617             : bool
    1618           7 : ConversationRepository::Impl::checkValidProfileUpdate(const std::string& userDevice,
    1619             :                                                       const std::string& commitId,
    1620             :                                                       const std::string& parentId) const
    1621             : {
    1622             :     // Retrieve tree for recent commit
    1623           7 :     auto repo = repository();
    1624           7 :     if (!repo)
    1625           0 :         return false;
    1626           7 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1627           7 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1628           7 :     if (not treeNew or not treeOld)
    1629           0 :         return false;
    1630             : 
    1631           7 :     auto userUri = uriFromDevice(userDevice, commitId);
    1632           7 :     if (userUri.empty())
    1633           0 :         return false;
    1634             : 
    1635             :     // Check if profile is changed by an user with correct privilege
    1636           7 :     auto valid = false;
    1637           7 :     if (updateProfilePermLvl_ == MemberRole::ADMIN) {
    1638           7 :         std::string adminFile = fmt::format("admins/{}.crt", userUri);
    1639           7 :         auto adminCert = fileAtTree(adminFile, treeNew);
    1640           7 :         valid |= adminCert != nullptr;
    1641           7 :     }
    1642           7 :     if (updateProfilePermLvl_ >= MemberRole::MEMBER) {
    1643           0 :         std::string memberFile = fmt::format("members/{}.crt", userUri);
    1644           0 :         auto memberCert = fileAtTree(memberFile, treeNew);
    1645           0 :         valid |= memberCert != nullptr;
    1646           0 :     }
    1647             : 
    1648           7 :     if (!valid) {
    1649           4 :         JAMI_ERROR("Profile changed from unauthorized user: {} ({})", userDevice, userUri);
    1650           1 :         return false;
    1651             :     }
    1652             : 
    1653           6 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1654             :     // Check that no weird file is added nor removed
    1655           6 :     std::string userDeviceFile = fmt::format("devices/{}.crt", userDevice);
    1656          12 :     for (const auto& f : changedFiles) {
    1657           7 :         if (f == "profile.vcf") {
    1658             :             // Ignore
    1659           2 :         } else if (f == userDeviceFile) {
    1660             :             // In this case, device is added or modified (certificate expiration)
    1661           1 :             auto oldFile = fileAtTree(f, treeOld);
    1662           1 :             std::string_view oldCert;
    1663           1 :             if (oldFile)
    1664           0 :                 oldCert = as_view(oldFile);
    1665           1 :             auto newFile = fileAtTree(f, treeNew);
    1666           1 :             if (!verifyCertificate(as_view(newFile), userUri, oldCert)) {
    1667           0 :                 JAMI_ERROR("Invalid certificate {}", f);
    1668           0 :                 return false;
    1669             :             }
    1670           1 :         } else {
    1671           4 :             JAMI_ERROR("Unwanted changed file detected: {}", f);
    1672           1 :             return false;
    1673             :         }
    1674             :     }
    1675           5 :     return true;
    1676           7 : }
    1677             : 
    1678             : /**
    1679             :  * @brief Get the deltas from a git diff
    1680             :  * @param diff The diff object to extract deltas from
    1681             :  * @return The set of git_diff deltas extracted from the diff
    1682             :  */
    1683             : std::optional<std::set<std::string_view>>
    1684          20 : ConversationRepository::Impl::getDeltaPathsFromDiff(const GitDiff& diff) const
    1685             : {
    1686          20 :     std::set<std::string_view> deltas_set = {};
    1687          68 :     for (size_t delta_idx = 0, delta_count = git_diff_num_deltas(diff.get()); delta_idx < delta_count; delta_idx++) {
    1688          48 :         const git_diff_delta* delta = git_diff_get_delta(diff.get(), delta_idx);
    1689          48 :         if (!delta) {
    1690           0 :             JAMI_LOG("[Account {}] [Conversation {}] Index of delta out of range!", accountId_, id_);
    1691           0 :             return std::nullopt;
    1692             :         }
    1693             : 
    1694          48 :         deltas_set.emplace(std::string_view(delta->old_file.path));
    1695          48 :         deltas_set.emplace(std::string_view(delta->new_file.path));
    1696             :     }
    1697          20 :     return deltas_set;
    1698          20 : }
    1699             : 
    1700             : /**
    1701             :  * @brief Validate the merge commit by ensuring the absence of invalid files
    1702             :  * @param mergeId The id of the merge commit
    1703             :  * @param parents The two commit IDs of parent's of the merge commit
    1704             :  * @return bool Whether or not invalid files were found in the merge commit
    1705             :  */
    1706             : bool
    1707          10 : ConversationRepository::Impl::checkValidMergeCommit(const std::string& mergeId,
    1708             :                                                     const std::vector<std::string>& parents) const
    1709             : {
    1710             :     // Get the repository associated with this implementation
    1711          10 :     auto repo = repository();
    1712          10 :     if (!repo)
    1713           0 :         return false;
    1714             : 
    1715             :     // Check for exactly two parents
    1716          10 :     if (static_cast<int>(parents.size()) != 2)
    1717           0 :         return false;
    1718             : 
    1719             :     // Get the tree of the merge commit
    1720          10 :     GitTree merge_commit_tree = treeAtCommit(repo.get(), mergeId);
    1721             : 
    1722             :     // Get the diff of the merge commit and the first parent
    1723          10 :     GitTree first_tree = treeAtCommit(repo.get(), parents[0]);
    1724          10 :     git_diff* diff_merge_tree_to_first_tree = nullptr;
    1725          10 :     if (git_diff_tree_to_tree(&diff_merge_tree_to_first_tree,
    1726             :                               repo.get(),
    1727             :                               first_tree.get(),
    1728             :                               merge_commit_tree.get(),
    1729             :                               nullptr)
    1730          10 :         < 0) {
    1731           0 :         const git_error* err = giterr_last();
    1732           0 :         if (err)
    1733           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Failed to git diff of merge and first parent "
    1734             :                        "failed: {}",
    1735             :                        accountId_,
    1736             :                        id_,
    1737             :                        err->message);
    1738           0 :         return false;
    1739             :     }
    1740          10 :     GitDiff first_diff {diff_merge_tree_to_first_tree};
    1741             : 
    1742             :     // Get the diff of the merge commit and the second parent
    1743          10 :     GitTree second_tree = treeAtCommit(repo.get(), parents[1]);
    1744          10 :     git_diff* diff_merge_tree_to_second_tree = nullptr;
    1745          10 :     if (git_diff_tree_to_tree(&diff_merge_tree_to_second_tree,
    1746             :                               repo.get(),
    1747             :                               second_tree.get(),
    1748             :                               merge_commit_tree.get(),
    1749             :                               nullptr)
    1750          10 :         < 0) {
    1751           0 :         const git_error* err = giterr_last();
    1752           0 :         if (err)
    1753           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Failed to git diff of merge and second parent "
    1754             :                        "failed: {}",
    1755             :                        accountId_,
    1756             :                        id_,
    1757             :                        err->message);
    1758           0 :         return false;
    1759             :     }
    1760          10 :     GitDiff second_diff {diff_merge_tree_to_second_tree};
    1761             : 
    1762             :     // Get the deltas of the first parent's commit
    1763          10 :     auto first_parent_deltas_set = getDeltaPathsFromDiff(first_diff);
    1764             :     // Get the deltas of the second parent's commit
    1765          10 :     auto second_parent_deltas_set = getDeltaPathsFromDiff(second_diff);
    1766          10 :     if (first_parent_deltas_set == std::nullopt || second_parent_deltas_set == std::nullopt) {
    1767           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get deltas from diffs for merge commit {}",
    1768             :                    accountId_,
    1769             :                    id_,
    1770             :                    mergeId);
    1771           0 :         return false;
    1772             :     }
    1773             :     // Get the intersection of the deltas of both parents
    1774          10 :     std::set<std::string_view> parent_deltas_intersection_set = {};
    1775          10 :     std::set_intersection(first_parent_deltas_set->begin(),
    1776             :                           first_parent_deltas_set->end(),
    1777             :                           second_parent_deltas_set->begin(),
    1778             :                           second_parent_deltas_set->end(),
    1779             :                           std::inserter(parent_deltas_intersection_set, parent_deltas_intersection_set.begin()));
    1780             : 
    1781             :     // The intersection of the set of diffs of both the parents of the merge commit should be be the
    1782             :     // empty set (i.e. no deltas in the intersection vector). This ensures that no malicious files
    1783             :     // have been added into the merge commit itself.
    1784          10 :     if (not parent_deltas_intersection_set.empty()) {
    1785           1 :         return false;
    1786             :     }
    1787           9 :     return true;
    1788          10 : }
    1789             : 
    1790             : bool
    1791        1725 : ConversationRepository::Impl::isValidUserAtCommit(const std::string& userDevice,
    1792             :                                                   const std::string& commitId,
    1793             :                                                   const git_buf& sig,
    1794             :                                                   const git_buf& sig_data) const
    1795             : {
    1796        1725 :     auto acc = account_.lock();
    1797        1724 :     if (!acc)
    1798           0 :         return false;
    1799        1724 :     auto cert = acc->certStore().getCertificate(userDevice);
    1800        1725 :     auto hasPinnedCert = cert and cert->issuer;
    1801        1725 :     auto repo = repository();
    1802        1725 :     if (not repo)
    1803           0 :         return false;
    1804             : 
    1805             :     // Retrieve tree for commit
    1806        1725 :     auto tree = treeAtCommit(repo.get(), commitId);
    1807        1725 :     if (not tree)
    1808           1 :         return false;
    1809             : 
    1810             :     // Check that /devices/userDevice.crt exists
    1811        1724 :     std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
    1812        1724 :     auto blob_device = fileAtTree(deviceFile, tree);
    1813        1724 :     if (!blob_device) {
    1814           4 :         JAMI_ERROR("{} announced but not found", deviceFile);
    1815           1 :         return false;
    1816             :     }
    1817        1723 :     auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
    1818        1723 :     auto userUri = deviceCert.getIssuerUID();
    1819        1723 :     if (userUri.empty()) {
    1820           0 :         JAMI_ERROR("{} got no issuer UID", deviceFile);
    1821           0 :         if (not hasPinnedCert) {
    1822           0 :             return false;
    1823             :         } else {
    1824             :             // HACK: JAMS device's certificate does not contains any issuer
    1825             :             // So, getIssuerUID() will be empty here, so there is no way
    1826             :             // to get the userURI from this certificate.
    1827             :             // Uses pinned certificate if one.
    1828           0 :             userUri = cert->issuer->getId().toString();
    1829             :         }
    1830             :     }
    1831             : 
    1832             :     // Check that /(members|admins)/userUri.crt exists
    1833        1722 :     auto blob_parent = memberCertificate(userUri, tree);
    1834        1723 :     if (not blob_parent) {
    1835           0 :         JAMI_ERROR("Certificate not found for {}", userUri);
    1836           0 :         return false;
    1837             :     }
    1838             : 
    1839             :     // Check that certificates were still valid
    1840        1723 :     auto parentCert = dht::crypto::Certificate(as_view(blob_parent));
    1841             : 
    1842             :     git_oid oid;
    1843        1723 :     git_commit* commit_ptr = nullptr;
    1844        1723 :     if (git_oid_fromstr(&oid, commitId.c_str()) < 0 || git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
    1845           0 :         JAMI_WARNING("Failed to look up commit {}", commitId);
    1846           0 :         return false;
    1847             :     }
    1848        1723 :     GitCommit commit {commit_ptr};
    1849             : 
    1850        1723 :     auto commitTime = std::chrono::system_clock::from_time_t(git_commit_time(commit.get()));
    1851        1723 :     if (deviceCert.getExpiration() < commitTime) {
    1852           0 :         JAMI_ERROR("Certificate {} expired", deviceCert.getId().toString());
    1853           0 :         return false;
    1854             :     }
    1855        1722 :     if (parentCert.getExpiration() < commitTime) {
    1856           0 :         JAMI_ERROR("Certificate {} expired", parentCert.getId().toString());
    1857           0 :         return false;
    1858             :     }
    1859             : 
    1860             :     //  Verify the signature (git verify-commit)
    1861        1723 :     auto pk = base64::decode(std::string_view(sig.ptr, sig.size));
    1862        1723 :     bool valid_signature = deviceCert.getPublicKey().checkSignature(reinterpret_cast<const uint8_t*>(sig_data.ptr),
    1863        1723 :                                                                     sig_data.size,
    1864        1723 :                                                                     pk.data(),
    1865             :                                                                     pk.size());
    1866             : 
    1867        1723 :     if (!valid_signature) {
    1868           4 :         JAMI_WARNING("Commit {} not signed by device {}.", git_oid_tostr_s(&oid), userDevice);
    1869           1 :         return false;
    1870             :     }
    1871             : 
    1872        1722 :     auto res = parentCert.getId().toString() == userUri;
    1873        1722 :     if (res && not hasPinnedCert) {
    1874          99 :         acc->certStore().pinCertificate(std::move(deviceCert));
    1875          99 :         acc->certStore().pinCertificate(std::move(parentCert));
    1876             :     }
    1877        1722 :     return res;
    1878        1725 : }
    1879             : 
    1880             : bool
    1881         188 : ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice,
    1882             :                                                  const std::string& commitId,
    1883             :                                                  const std::string& commitMsg) const
    1884             : {
    1885         188 :     auto account = account_.lock();
    1886         188 :     auto repo = repository();
    1887         188 :     if (not account or not repo) {
    1888           0 :         JAMI_WARNING("Invalid repository detected");
    1889           0 :         return false;
    1890             :     }
    1891             : 
    1892         188 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1893         188 :     auto userUri = uriFromDevice(userDevice, commitId);
    1894         188 :     if (userUri.empty())
    1895           0 :         return false;
    1896             : 
    1897         376 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, ""));
    1898             :     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1899             : 
    1900             :     try {
    1901         188 :         mode();
    1902           0 :     } catch (...) {
    1903           0 :         JAMI_ERROR("Invalid mode detected for commit: {}", commitId);
    1904           0 :         return false;
    1905           0 :     }
    1906             : 
    1907         188 :     std::string invited = {};
    1908         188 :     if (mode_ == ConversationMode::ONE_TO_ONE) {
    1909          59 :         Json::Value cm;
    1910          59 :         if (json::parse(commitMsg, cm)) {
    1911          59 :             invited = cm["invited"].asString();
    1912             :         }
    1913          59 :     }
    1914             : 
    1915         188 :     auto hasDevice = false, hasAdmin = false;
    1916         188 :     std::string adminsFile = fmt::format("admins/{}.crt", userUri);
    1917         188 :     std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
    1918         188 :     std::string crlFile = fmt::format("CRLs/{}", userUri);
    1919         188 :     std::string invitedFile = fmt::format("invited/{}", invited);
    1920             : 
    1921             :     // Check that admin cert is added
    1922             :     // Check that device cert is added
    1923             :     // Check CRLs added
    1924             :     // Check that no other file is added
    1925             :     // Check if invited file present for one to one.
    1926         620 :     for (const auto& changedFile : changedFiles) {
    1927         434 :         if (changedFile == adminsFile) {
    1928         187 :             hasAdmin = true;
    1929         187 :             auto newFile = fileAtTree(changedFile, treeNew);
    1930         187 :             if (!verifyCertificate(as_view(newFile), userUri)) {
    1931           0 :                 JAMI_ERROR("Invalid certificate found {}", changedFile);
    1932           0 :                 return false;
    1933             :             }
    1934         434 :         } else if (changedFile == deviceFile) {
    1935         187 :             hasDevice = true;
    1936         187 :             auto newFile = fileAtTree(changedFile, treeNew);
    1937         187 :             if (!verifyCertificate(as_view(newFile), userUri)) {
    1938           4 :                 JAMI_ERROR("Invalid certificate found {}", changedFile);
    1939           1 :                 return false;
    1940             :             }
    1941         247 :         } else if (changedFile == crlFile || changedFile == invitedFile) {
    1942             :             // Nothing to do
    1943          59 :             continue;
    1944             :         } else {
    1945             :             // Invalid file detected
    1946           4 :             JAMI_ERROR("Invalid add file detected: {} {}", changedFile, (int) *mode_);
    1947           1 :             return false;
    1948             :         }
    1949             :     }
    1950             : 
    1951         186 :     return hasDevice && hasAdmin;
    1952         188 : }
    1953             : 
    1954             : bool
    1955         525 : ConversationRepository::Impl::validateDevice()
    1956             : {
    1957         525 :     auto repo = repository();
    1958         525 :     auto account = account_.lock();
    1959         525 :     if (!account || !repo) {
    1960           0 :         JAMI_WARNING("[Account {}] [Conversation {}] Invalid repository detected", accountId_, id_);
    1961           0 :         return false;
    1962             :     }
    1963         525 :     auto path = fmt::format("devices/{}.crt", deviceId_);
    1964         525 :     std::filesystem::path devicePath = git_repository_workdir(repo.get());
    1965         525 :     devicePath /= path;
    1966         525 :     if (!std::filesystem::is_regular_file(devicePath)) {
    1967           0 :         JAMI_WARNING("[Account {}] [Conversation {}] Unable to find file {}", accountId_, id_, devicePath);
    1968           0 :         return false;
    1969             :     }
    1970             : 
    1971         525 :     auto wrongDeviceFile = false;
    1972             :     try {
    1973        1050 :         auto deviceCert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
    1974         525 :         wrongDeviceFile = !account->isValidAccountDevice(deviceCert);
    1975         525 :     } catch (const std::exception&) {
    1976           0 :         wrongDeviceFile = true;
    1977           0 :     }
    1978         525 :     if (wrongDeviceFile) {
    1979          12 :         JAMI_WARNING(
    1980             :             "[Account {}] [Conversation {}] Device certificate is no longer valid. Attempting to update certificate.",
    1981             :             accountId_,
    1982             :             id_);
    1983             :         // Replace certificate with current cert
    1984           3 :         auto cert = account->identity().second;
    1985           3 :         if (!cert || !account->isValidAccountDevice(*cert)) {
    1986           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Current device's certificate is invalid. A migration is needed",
    1987             :                        accountId_,
    1988             :                        id_);
    1989           0 :             return false;
    1990             :         }
    1991           3 :         std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
    1992           3 :         if (!file.is_open()) {
    1993           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to write data to {}", accountId_, id_, devicePath);
    1994           0 :             return false;
    1995             :         }
    1996           3 :         file << cert->toString(false);
    1997           3 :         file.close();
    1998           3 :         if (!add(path)) {
    1999           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to add file {}", accountId_, id_, devicePath);
    2000           0 :             return false;
    2001             :         }
    2002           3 :     }
    2003             : 
    2004             :     // Check account cert (a new device can be added but account certifcate can be the old one!)
    2005         525 :     auto adminPath = fmt::format("admins/{}.crt", userId_);
    2006         525 :     auto memberPath = fmt::format("members/{}.crt", userId_);
    2007         525 :     std::filesystem::path parentPath = git_repository_workdir(repo.get());
    2008         525 :     std::filesystem::path relativeParentPath;
    2009         525 :     if (std::filesystem::is_regular_file(parentPath / adminPath))
    2010         307 :         relativeParentPath = adminPath;
    2011         218 :     else if (std::filesystem::is_regular_file(parentPath / memberPath))
    2012         217 :         relativeParentPath = memberPath;
    2013         525 :     parentPath /= relativeParentPath;
    2014         525 :     if (relativeParentPath.empty()) {
    2015           4 :         JAMI_ERROR("[Account {}] [Conversation {}] Invalid parent path (not in members or admins)", accountId_, id_);
    2016           1 :         return false;
    2017             :     }
    2018         524 :     wrongDeviceFile = false;
    2019             :     try {
    2020        1048 :         auto parentCert = dht::crypto::Certificate(fileutils::loadFile(parentPath));
    2021         524 :         wrongDeviceFile = !account->isValidAccountDevice(parentCert);
    2022         524 :     } catch (const std::exception&) {
    2023           0 :         wrongDeviceFile = true;
    2024           0 :     }
    2025         524 :     if (wrongDeviceFile) {
    2026           4 :         JAMI_WARNING(
    2027             :             "[Account {}] [Conversation {}] Account certificate is no longer valid. Attempting to update certificate.",
    2028             :             accountId_,
    2029             :             id_);
    2030           1 :         auto cert = account->identity().second;
    2031           1 :         auto newCert = cert->issuer;
    2032           1 :         if (newCert && std::filesystem::is_regular_file(parentPath)) {
    2033           1 :             std::ofstream file(parentPath, std::ios::trunc | std::ios::binary);
    2034           1 :             if (!file.is_open()) {
    2035           0 :                 JAMI_ERROR("Unable to write data to {}", path);
    2036           0 :                 return false;
    2037             :             }
    2038           1 :             file << newCert->toString(true);
    2039           1 :             file.close();
    2040           1 :             if (!add(relativeParentPath.string())) {
    2041           0 :                 JAMI_WARNING("Unable to add file {}", path);
    2042           0 :                 return false;
    2043             :             }
    2044           1 :         }
    2045           1 :     }
    2046             : 
    2047         524 :     return true;
    2048         525 : }
    2049             : 
    2050             : std::string
    2051         494 : ConversationRepository::Impl::commit(const std::string& msg, bool verifyDevice)
    2052             : {
    2053         494 :     if (verifyDevice && !validateDevice()) {
    2054           4 :         JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Invalid device", accountId_, id_);
    2055           1 :         return {};
    2056             :     }
    2057         493 :     GitSignature sig = signature();
    2058         493 :     if (!sig) {
    2059           0 :         JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to generate signature", accountId_, id_);
    2060           0 :         return {};
    2061             :     }
    2062         493 :     auto account = account_.lock();
    2063             : 
    2064             :     // Retrieve current index
    2065         493 :     git_index* index_ptr = nullptr;
    2066         493 :     auto repo = repository();
    2067         493 :     if (!repo)
    2068           0 :         return {};
    2069         493 :     if (git_repository_index(&index_ptr, repo.get()) < 0) {
    2070           0 :         JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to open repository index", accountId_, id_);
    2071           0 :         return {};
    2072             :     }
    2073         493 :     GitIndex index {index_ptr};
    2074             : 
    2075             :     git_oid tree_id;
    2076         493 :     if (git_index_write_tree(&tree_id, index.get()) < 0) {
    2077           0 :         JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to write initial tree from index",
    2078             :                    accountId_,
    2079             :                    id_);
    2080           0 :         return {};
    2081             :     }
    2082             : 
    2083         493 :     git_tree* tree_ptr = nullptr;
    2084         493 :     if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) {
    2085           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up initial tree", accountId_, id_);
    2086           0 :         return {};
    2087             :     }
    2088         493 :     GitTree tree {tree_ptr};
    2089             : 
    2090             :     git_oid commit_id;
    2091         493 :     if (git_reference_name_to_id(&commit_id, repo.get(), "HEAD") < 0) {
    2092           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
    2093           0 :         return {};
    2094             :     }
    2095             : 
    2096         493 :     git_commit* head_ptr = nullptr;
    2097         493 :     if (git_commit_lookup(&head_ptr, repo.get(), &commit_id) < 0) {
    2098           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", accountId_, id_);
    2099           0 :         return {};
    2100             :     }
    2101         493 :     GitCommit head_commit {head_ptr};
    2102             : 
    2103         493 :     git_buf to_sign = {};
    2104             :     // The last argument of git_commit_create_buffer is of type
    2105             :     // 'const git_commit **' in all versions of libgit2 except 1.8.0,
    2106             :     // 1.8.1 and 1.8.3, in which it is of type 'git_commit *const *'.
    2107             : #if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
    2108             :     && (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
    2109         493 :     git_commit* const head_ref[1] = {head_commit.get()};
    2110             : #else
    2111             :     const git_commit* head_ref[1] = {head_commit.get()};
    2112             : #endif
    2113         986 :     if (git_commit_create_buffer(
    2114         986 :             &to_sign, repo.get(), sig.get(), sig.get(), nullptr, msg.c_str(), tree.get(), 1, &head_ref[0])
    2115         493 :         < 0) {
    2116           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to create commit buffer", accountId_, id_);
    2117           0 :         return {};
    2118             :     }
    2119             : 
    2120             :     // git commit -S
    2121         493 :     auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
    2122         493 :     auto signed_buf = account->identity().first->sign(to_sign_vec);
    2123         493 :     std::string signed_str = base64::encode(signed_buf);
    2124         493 :     if (git_commit_create_with_signature(&commit_id, repo.get(), to_sign.ptr, signed_str.c_str(), "signature") < 0) {
    2125           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to sign commit", accountId_, id_);
    2126           0 :         git_buf_dispose(&to_sign);
    2127           0 :         return {};
    2128             :     }
    2129         493 :     git_buf_dispose(&to_sign);
    2130             : 
    2131             :     // Move commit to main branch
    2132         493 :     git_reference* ref_ptr = nullptr;
    2133         493 :     if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_id, true, nullptr) < 0) {
    2134           0 :         const git_error* err = giterr_last();
    2135           0 :         if (err) {
    2136           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to move commit to main: {}",
    2137             :                        accountId_,
    2138             :                        id_,
    2139             :                        err->message);
    2140           0 :             emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, ECOMMIT, err->message);
    2141             :         }
    2142           0 :         return {};
    2143             :     }
    2144         493 :     git_reference_free(ref_ptr);
    2145             : 
    2146         493 :     auto commit_str = git_oid_tostr_s(&commit_id);
    2147         493 :     if (commit_str) {
    2148        1972 :         JAMI_LOG("[Account {}] [Conversation {}] New message added with id: {}", accountId_, id_, commit_str);
    2149             :     }
    2150         493 :     return commit_str ? commit_str : "";
    2151         493 : }
    2152             : 
    2153             : ConversationMode
    2154        7533 : ConversationRepository::Impl::mode() const
    2155             : {
    2156             :     // If already retrieved, return it, else get it from first commit
    2157        7533 :     if (mode_ != std::nullopt)
    2158        7118 :         return *mode_;
    2159             : 
    2160         415 :     auto initialCommit = getCommit(id_);
    2161         415 :     if (!initialCommit) {
    2162           1 :         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, EINVALIDMODE, "No initial commit");
    2163           1 :         throw std::logic_error("Unable to retrieve first commit");
    2164             :     }
    2165         414 :     auto commitMsg = initialCommit->commit_msg;
    2166             : 
    2167         414 :     Json::Value root;
    2168         414 :     if (!json::parse(commitMsg, root)) {
    2169           0 :         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, EINVALIDMODE, "No initial commit");
    2170           0 :         throw std::logic_error("Unable to retrieve first commit");
    2171             :     }
    2172         414 :     if (!root.isMember("mode")) {
    2173           0 :         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, EINVALIDMODE, "No mode detected");
    2174           0 :         throw std::logic_error("No mode detected for initial commit");
    2175             :     }
    2176         414 :     int mode = root["mode"].asInt();
    2177             : 
    2178         414 :     switch (mode) {
    2179         134 :     case 0:
    2180         134 :         mode_ = ConversationMode::ONE_TO_ONE;
    2181         134 :         break;
    2182           6 :     case 1:
    2183           6 :         mode_ = ConversationMode::ADMIN_INVITES_ONLY;
    2184           6 :         break;
    2185         274 :     case 2:
    2186         274 :         mode_ = ConversationMode::INVITES_ONLY;
    2187         274 :         break;
    2188           0 :     case 3:
    2189           0 :         mode_ = ConversationMode::PUBLIC;
    2190           0 :         break;
    2191           0 :     default:
    2192           0 :         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2193           0 :                                                                      id_,
    2194             :                                                                      EINVALIDMODE,
    2195             :                                                                      "Incorrect mode detected");
    2196           0 :         throw std::logic_error("Incorrect mode detected");
    2197             :     }
    2198         414 :     return *mode_;
    2199         415 : }
    2200             : 
    2201             : std::string
    2202        2827 : ConversationRepository::Impl::diffStats(const std::string& newId, const std::string& oldId) const
    2203             : {
    2204        2827 :     if (auto repo = repository()) {
    2205        2827 :         if (auto d = diff(repo.get(), newId, oldId))
    2206        2827 :             return diffStats(d);
    2207        2827 :     }
    2208           0 :     return {};
    2209             : }
    2210             : 
    2211             : GitDiff
    2212        2827 : ConversationRepository::Impl::diff(git_repository* repo, const std::string& idNew, const std::string& idOld) const
    2213             : {
    2214        2827 :     if (!repo) {
    2215           0 :         JAMI_ERROR("Unable to get reference for HEAD");
    2216           0 :         return nullptr;
    2217             :     }
    2218             : 
    2219             :     // Retrieve tree for commit new
    2220             :     git_oid oid;
    2221        2827 :     git_commit* commitNew = nullptr;
    2222        2827 :     if (idNew == "HEAD") {
    2223         909 :         if (git_reference_name_to_id(&oid, repo, "HEAD") < 0) {
    2224           0 :             JAMI_ERROR("Unable to get reference for HEAD");
    2225           0 :             return nullptr;
    2226             :         }
    2227             : 
    2228         909 :         if (git_commit_lookup(&commitNew, repo, &oid) < 0) {
    2229           0 :             JAMI_ERROR("Unable to look up HEAD commit");
    2230           0 :             return nullptr;
    2231             :         }
    2232             :     } else {
    2233        1918 :         if (git_oid_fromstr(&oid, idNew.c_str()) < 0 || git_commit_lookup(&commitNew, repo, &oid) < 0) {
    2234           0 :             GitCommit new_commit {commitNew};
    2235           0 :             JAMI_WARNING("Failed to look up commit {}", idNew);
    2236           0 :             return nullptr;
    2237           0 :         }
    2238             :     }
    2239        2827 :     GitCommit new_commit {commitNew};
    2240             : 
    2241        2827 :     git_tree* tNew = nullptr;
    2242        2827 :     if (git_commit_tree(&tNew, new_commit.get()) < 0) {
    2243           0 :         JAMI_ERROR("Unable to look up initial tree");
    2244           0 :         return nullptr;
    2245             :     }
    2246        2827 :     GitTree treeNew {tNew};
    2247             : 
    2248        2827 :     git_diff* diff_ptr = nullptr;
    2249        2827 :     if (idOld.empty()) {
    2250         189 :         if (git_diff_tree_to_tree(&diff_ptr, repo, nullptr, treeNew.get(), {}) < 0) {
    2251           0 :             JAMI_ERROR("Unable to get diff to empty repository");
    2252           0 :             return nullptr;
    2253             :         }
    2254         189 :         return GitDiff(diff_ptr);
    2255             :     }
    2256             : 
    2257             :     // Retrieve tree for commit old
    2258        2638 :     git_commit* commitOld = nullptr;
    2259        2638 :     if (git_oid_fromstr(&oid, idOld.c_str()) < 0 || git_commit_lookup(&commitOld, repo, &oid) < 0) {
    2260           0 :         JAMI_WARNING("Failed to look up commit {}", idOld);
    2261           0 :         return nullptr;
    2262             :     }
    2263        2638 :     GitCommit old_commit {commitOld};
    2264             : 
    2265        2638 :     git_tree* tOld = nullptr;
    2266        2638 :     if (git_commit_tree(&tOld, old_commit.get()) < 0) {
    2267           0 :         JAMI_ERROR("Unable to look up initial tree");
    2268           0 :         return nullptr;
    2269             :     }
    2270        2638 :     GitTree treeOld {tOld};
    2271             : 
    2272             :     // Calc diff
    2273        2638 :     if (git_diff_tree_to_tree(&diff_ptr, repo, treeOld.get(), treeNew.get(), {}) < 0) {
    2274           0 :         JAMI_ERROR("Unable to get diff between {} and {}", idOld, idNew);
    2275           0 :         return nullptr;
    2276             :     }
    2277        2638 :     return GitDiff(diff_ptr);
    2278        2827 : }
    2279             : 
    2280             : std::vector<ConversationCommit>
    2281        1919 : ConversationRepository::Impl::behind(const std::string& from) const
    2282             : {
    2283             :     git_oid oid_local, oid_head, oid_remote;
    2284        1919 :     auto repo = repository();
    2285        1919 :     if (!repo)
    2286           0 :         return {};
    2287        1919 :     if (git_reference_name_to_id(&oid_local, repo.get(), "HEAD") < 0) {
    2288           0 :         JAMI_ERROR("Unable to get reference for HEAD");
    2289           0 :         return {};
    2290             :     }
    2291        1919 :     oid_head = oid_local;
    2292        1919 :     std::string head = git_oid_tostr_s(&oid_head);
    2293        1919 :     if (git_oid_fromstr(&oid_remote, from.c_str()) < 0) {
    2294           0 :         JAMI_ERROR("Unable to get reference for commit {}", from);
    2295           0 :         return {};
    2296             :     }
    2297             : 
    2298             :     git_oidarray bases;
    2299        1919 :     if (git_merge_bases(&bases, repo.get(), &oid_local, &oid_remote) != 0) {
    2300           0 :         JAMI_ERROR("Unable to get any merge base for commit {} and {}", from, head);
    2301           0 :         return {};
    2302             :     }
    2303        3774 :     for (std::size_t i = 0; i < bases.count; ++i) {
    2304        1919 :         std::string oid = git_oid_tostr_s(&bases.ids[i]);
    2305        1918 :         if (oid != head) {
    2306          64 :             oid_local = bases.ids[i];
    2307          64 :             break;
    2308             :         }
    2309        1919 :     }
    2310        1919 :     git_oidarray_free(&bases);
    2311        1919 :     std::string to = git_oid_tostr_s(&oid_local);
    2312        1918 :     if (to == from)
    2313         993 :         return {};
    2314        1852 :     return log(LogOptions {from, to});
    2315        1919 : }
    2316             : 
    2317             : void
    2318        2687 : ConversationRepository::Impl::forEachCommit(PreConditionCb&& preCondition,
    2319             :                                             std::function<void(ConversationCommit&&)>&& emplaceCb,
    2320             :                                             PostConditionCb&& postCondition,
    2321             :                                             const std::string& from,
    2322             :                                             bool logIfNotFound) const
    2323             : {
    2324             :     git_oid oid, oidFrom, oidMerge;
    2325             : 
    2326             :     // NOTE! Start from head to get all merge possibilities and correct linearized parent.
    2327        2687 :     auto repo = repository();
    2328        2687 :     if (!repo or git_reference_name_to_id(&oid, repo.get(), "HEAD") < 0) {
    2329           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
    2330           0 :         return;
    2331             :     }
    2332             : 
    2333        2687 :     if (from != "" && git_oid_fromstr(&oidFrom, from.c_str()) == 0) {
    2334        2043 :         auto isMergeBase = git_merge_base(&oidMerge, repo.get(), &oid, &oidFrom) == 0
    2335        2044 :                            && git_oid_equal(&oidMerge, &oidFrom);
    2336        2044 :         if (!isMergeBase) {
    2337             :             // We're logging a non merged branch, so, take this one instead of HEAD
    2338         936 :             oid = oidFrom;
    2339             :         }
    2340             :     }
    2341             : 
    2342        2686 :     git_revwalk* walker_ptr = nullptr;
    2343        2686 :     if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
    2344           7 :         GitRevWalker walker {walker_ptr};
    2345             :         // This fail can be permitted in the case we check if a commit exists before pulling (so can fail
    2346             :         // there). Only log if the fail is unwanted.
    2347           7 :         if (logIfNotFound)
    2348           0 :             JAMI_DEBUG("[Account {}] [Conversation {}] Unable to init revwalker from {}", accountId_, id_, from);
    2349           7 :         return;
    2350           7 :     }
    2351             : 
    2352        2680 :     GitRevWalker walker {walker_ptr};
    2353        2680 :     git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
    2354             : 
    2355       10849 :     while (!git_revwalk_next(&oid, walker.get())) {
    2356       10131 :         git_commit* commit_ptr = nullptr;
    2357       10131 :         std::string id = git_oid_tostr_s(&oid);
    2358       10128 :         if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
    2359           0 :             JAMI_WARNING("[Account {}] [Conversation {}] Failed to look up commit {}", accountId_, id_, id);
    2360           0 :             break;
    2361             :         }
    2362       10131 :         GitCommit commit {commit_ptr};
    2363             : 
    2364       10130 :         ConversationCommit cc = parseCommit(repo.get(), commit.get());
    2365             : 
    2366       10113 :         auto result = preCondition(id, cc.author, commit);
    2367       10121 :         if (result == CallbackResult::Skip)
    2368          33 :             continue;
    2369       10088 :         else if (result == CallbackResult::Break)
    2370        1656 :             break;
    2371             : 
    2372        8432 :         auto post = postCondition(id, cc.author, cc);
    2373        8437 :         emplaceCb(std::move(cc));
    2374             : 
    2375        8440 :         if (post)
    2376         302 :             break;
    2377       14109 :     }
    2378        2686 : }
    2379             : 
    2380             : std::vector<ConversationCommit>
    2381        1127 : ConversationRepository::Impl::log(const LogOptions& options) const
    2382             : {
    2383        1127 :     std::vector<ConversationCommit> commits {};
    2384        1127 :     auto startLogging = options.from == "";
    2385        1127 :     auto breakLogging = false;
    2386        1128 :     forEachCommit(
    2387        2911 :         [&](const auto& id, const auto& author, const auto& commit) {
    2388        2911 :             if (!commits.empty()) {
    2389             :                 // Set linearized parent
    2390        1776 :                 commits.rbegin()->linearized_parent = id;
    2391             :             }
    2392        2910 :             if (options.skipMerge && git_commit_parentcount(commit.get()) > 1) {
    2393           0 :                 return CallbackResult::Skip;
    2394             :             }
    2395        2910 :             if ((options.nbOfCommits != 0 && commits.size() == options.nbOfCommits))
    2396           1 :                 return CallbackResult::Break; // Stop logging
    2397        2909 :             if (breakLogging)
    2398           0 :                 return CallbackResult::Break; // Stop logging
    2399        2909 :             if (id == options.to) {
    2400         925 :                 if (options.includeTo)
    2401           0 :                     breakLogging = true; // For the next commit
    2402             :                 else
    2403         925 :                     return CallbackResult::Break; // Stop logging
    2404             :             }
    2405             : 
    2406        1984 :             if (!startLogging && options.from != "" && options.from == id)
    2407         929 :                 startLogging = true;
    2408        1984 :             if (!startLogging)
    2409           7 :                 return CallbackResult::Skip; // Start logging after this one
    2410             : 
    2411        1977 :             if (options.fastLog) {
    2412           0 :                 if (options.authorUri != "") {
    2413           0 :                     if (options.authorUri == uriFromDevice(author.email)) {
    2414           0 :                         return CallbackResult::Break; // Found author, stop
    2415             :                     }
    2416             :                 }
    2417             :                 // Used to only count commit
    2418           0 :                 commits.emplace(commits.end(), ConversationCommit {});
    2419           0 :                 return CallbackResult::Skip;
    2420             :             }
    2421             : 
    2422        1977 :             return CallbackResult::Ok; // Continue
    2423             :         },
    2424        1977 :         [&](auto&& cc) { commits.emplace(commits.end(), std::forward<decltype(cc)>(cc)); },
    2425        1976 :         [](auto, auto, auto) { return false; },
    2426        1127 :         options.from,
    2427        1127 :         options.logIfNotFound);
    2428        2256 :     return commits;
    2429           0 : }
    2430             : 
    2431             : GitObject
    2432       11785 : ConversationRepository::Impl::fileAtTree(const std::string& path, const GitTree& tree) const
    2433             : {
    2434       11785 :     git_object* blob_ptr = nullptr;
    2435       11785 :     if (git_object_lookup_bypath(&blob_ptr, reinterpret_cast<git_object*>(tree.get()), path.c_str(), GIT_OBJECT_BLOB)
    2436       11788 :         != 0) {
    2437        3237 :         return GitObject(nullptr);
    2438             :     }
    2439        8551 :     return GitObject(blob_ptr);
    2440             : }
    2441             : 
    2442             : GitObject
    2443        1725 : ConversationRepository::Impl::memberCertificate(std::string_view memberUri, const GitTree& tree) const
    2444             : {
    2445        3450 :     auto blob = fileAtTree(fmt::format("members/{}.crt", memberUri), tree);
    2446        1728 :     if (not blob)
    2447        1786 :         blob = fileAtTree(fmt::format("admins/{}.crt", memberUri), tree);
    2448        1728 :     return blob;
    2449           0 : }
    2450             : 
    2451             : GitTree
    2452        5079 : ConversationRepository::Impl::treeAtCommit(git_repository* repo, const std::string& commitId) const
    2453             : {
    2454             :     git_oid oid;
    2455        5079 :     git_commit* commit = nullptr;
    2456        5079 :     if (git_oid_fromstr(&oid, commitId.c_str()) < 0 || git_commit_lookup(&commit, repo, &oid) < 0) {
    2457           4 :         JAMI_WARNING("[Account {}] [Conversation {}] Failed to look up commit {}", accountId_, id_, commitId);
    2458           1 :         return GitTree(nullptr);
    2459             :     }
    2460        5079 :     GitCommit gc {commit};
    2461        5079 :     git_tree* tree = nullptr;
    2462        5079 :     if (git_commit_tree(&tree, gc.get()) < 0) {
    2463           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up initial tree", accountId_, id_);
    2464           0 :         return GitTree(nullptr);
    2465             :     }
    2466        5079 :     return GitTree {tree};
    2467        5079 : }
    2468             : 
    2469             : std::vector<std::string>
    2470         198 : ConversationRepository::Impl::getInitialMembers() const
    2471             : {
    2472         198 :     auto acc = account_.lock();
    2473         198 :     if (!acc)
    2474           0 :         return {};
    2475         198 :     auto firstCommitOpt = getCommit(id_);
    2476         198 :     if (firstCommitOpt == std::nullopt) {
    2477           0 :         return {};
    2478             :     }
    2479         198 :     auto& commit = *firstCommitOpt;
    2480             : 
    2481         198 :     auto authorDevice = commit.author.email;
    2482         198 :     auto cert = acc->certStore().getCertificate(authorDevice);
    2483         198 :     if (!cert || !cert->issuer)
    2484           3 :         return {};
    2485         195 :     auto authorId = cert->issuer->getId().toString();
    2486         195 :     if (mode() == ConversationMode::ONE_TO_ONE) {
    2487         195 :         Json::Value root;
    2488         195 :         if (!json::parse(commit.commit_msg, root)) {
    2489           0 :             return {authorId};
    2490             :         }
    2491         195 :         if (root.isMember("invited") && root["invited"].asString() != authorId)
    2492         582 :             return {authorId, root["invited"].asString()};
    2493         195 :     }
    2494           2 :     return {authorId};
    2495         198 : }
    2496             : 
    2497             : bool
    2498           1 : ConversationRepository::Impl::resolveConflicts(git_index* index, const std::string& other_id)
    2499             : {
    2500           1 :     git_index_conflict_iterator* conflict_iterator = nullptr;
    2501           1 :     const git_index_entry* ancestor_out = nullptr;
    2502           1 :     const git_index_entry* our_out = nullptr;
    2503           1 :     const git_index_entry* their_out = nullptr;
    2504             : 
    2505           1 :     git_index_conflict_iterator_new(&conflict_iterator, index);
    2506           1 :     GitIndexConflictIterator ci {conflict_iterator};
    2507             : 
    2508             :     git_oid head_commit_id;
    2509           1 :     auto repo = repository();
    2510           1 :     if (!repo || git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD") < 0) {
    2511           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
    2512           0 :         return false;
    2513             :     }
    2514           1 :     auto commit_str = git_oid_tostr_s(&head_commit_id);
    2515           1 :     if (!commit_str)
    2516           0 :         return false;
    2517           1 :     auto useRemote = (other_id > commit_str); // Choose by commit version
    2518             : 
    2519             :     // NOTE: for now, only authorize conflicts on "profile.vcf"
    2520           1 :     std::vector<git_index_entry> new_entries;
    2521           2 :     while (git_index_conflict_next(&ancestor_out, &our_out, &their_out, ci.get()) != GIT_ITEROVER) {
    2522           1 :         if (ancestor_out && ancestor_out->path && our_out && our_out->path && their_out && their_out->path) {
    2523           1 :             if (std::string_view(ancestor_out->path) == "profile.vcf"sv) {
    2524             :                 // Checkout the wanted version. Copy the index_entry.
    2525           1 :                 git_index_entry resolution = useRemote ? *their_out : *our_out;
    2526           1 :                 resolution.flags &= GIT_INDEX_STAGE_NORMAL;
    2527           1 :                 if (!(resolution.flags & GIT_IDXENTRY_VALID))
    2528           1 :                     resolution.flags |= GIT_IDXENTRY_VALID;
    2529             :                 // NOTE: do no git_index_add yet, wait for after full conflict checks
    2530           1 :                 new_entries.push_back(resolution);
    2531           1 :                 continue;
    2532           1 :             }
    2533           0 :             JAMI_ERROR("Conflict detected on a file that is not authorized: {}", ancestor_out->path);
    2534           0 :             return false;
    2535             :         }
    2536           0 :         return false;
    2537             :     }
    2538             : 
    2539           2 :     for (auto& entry : new_entries)
    2540           1 :         git_index_add(index, &entry);
    2541           1 :     git_index_conflict_cleanup(index);
    2542             : 
    2543             :     // Checkout and clean up
    2544             :     git_checkout_options opt;
    2545           1 :     git_checkout_options_init(&opt, GIT_CHECKOUT_OPTIONS_VERSION);
    2546           1 :     opt.checkout_strategy |= GIT_CHECKOUT_FORCE;
    2547           1 :     opt.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS;
    2548           1 :     if (other_id > commit_str)
    2549           1 :         opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS;
    2550             :     else
    2551           0 :         opt.checkout_strategy |= GIT_CHECKOUT_USE_OURS;
    2552             : 
    2553           1 :     if (git_checkout_index(repo.get(), index, &opt) < 0) {
    2554           0 :         const git_error* err = giterr_last();
    2555           0 :         if (err)
    2556           0 :             JAMI_ERROR("Unable to checkout index: {}", err->message);
    2557           0 :         return false;
    2558             :     }
    2559             : 
    2560           1 :     return true;
    2561           1 : }
    2562             : 
    2563             : void
    2564        1186 : ConversationRepository::Impl::initMembers()
    2565             : {
    2566             :     using std::filesystem::path;
    2567        1186 :     auto repo = repository();
    2568        1186 :     if (!repo)
    2569           0 :         throw std::logic_error("Invalid Git repository");
    2570             : 
    2571        1186 :     std::vector<std::string> uris;
    2572        1186 :     std::lock_guard lk(membersMtx_);
    2573        1186 :     members_.clear();
    2574        1186 :     path repoPath = git_repository_workdir(repo.get());
    2575             : 
    2576           0 :     static const std::vector<std::pair<MemberRole, path>> paths = {{MemberRole::ADMIN, MemberPath::ADMINS},
    2577           0 :                                                                    {MemberRole::MEMBER, MemberPath::MEMBERS},
    2578           0 :                                                                    {MemberRole::INVITED, MemberPath::INVITED},
    2579           0 :                                                                    {MemberRole::BANNED,
    2580          26 :                                                                     MemberPath::BANNED / MemberPath::MEMBERS},
    2581           0 :                                                                    {MemberRole::BANNED,
    2582        1264 :                                                                     MemberPath::BANNED / MemberPath::INVITED}};
    2583             : 
    2584        1186 :     std::error_code ec;
    2585        7116 :     for (const auto& [role, p] : paths) {
    2586       30472 :         for (const auto& f : std::filesystem::directory_iterator(repoPath / p, ec)) {
    2587       12701 :             auto uri = f.path().stem().string();
    2588       12685 :             if (std::find(uris.begin(), uris.end(), uri) == uris.end()) {
    2589       12697 :                 members_.emplace_back(ConversationMember {uri, role});
    2590       12685 :                 uris.emplace_back(uri);
    2591             :             }
    2592       18625 :         }
    2593             :     }
    2594             : 
    2595        1185 :     if (mode() == ConversationMode::ONE_TO_ONE) {
    2596         494 :         for (const auto& member : getInitialMembers()) {
    2597         327 :             if (std::find(uris.begin(), uris.end(), member) == uris.end()) {
    2598             :                 // If member is in the initial commit, but not in invited, this means that user left.
    2599           0 :                 members_.emplace_back(ConversationMember {member, MemberRole::LEFT});
    2600             :             }
    2601         167 :         }
    2602             :     }
    2603        1184 :     saveMembers();
    2604        1189 : }
    2605             : 
    2606             : std::optional<std::map<std::string, std::string>>
    2607        8427 : ConversationRepository::Impl::convCommitToMap(const ConversationCommit& commit) const
    2608             : {
    2609        8427 :     auto authorId = uriFromDevice(commit.author.email, commit.id);
    2610        8428 :     if (authorId.empty()) {
    2611           4 :         JAMI_ERROR("[Account {}] [Conversation {}] Invalid author ID for commit {}", accountId_, id_, commit.id);
    2612           1 :         return std::nullopt;
    2613             :     }
    2614        8426 :     std::string parents;
    2615        8427 :     auto parentsSize = commit.parents.size();
    2616       16182 :     for (std::size_t i = 0; i < parentsSize; ++i) {
    2617        7755 :         parents += commit.parents[i];
    2618        7755 :         if (i != parentsSize - 1)
    2619          36 :             parents += ",";
    2620             :     }
    2621        8427 :     std::string type {};
    2622        8426 :     if (parentsSize > 1)
    2623          36 :         type = "merge";
    2624        8426 :     std::string body {};
    2625        8426 :     std::map<std::string, std::string> message;
    2626        8425 :     if (type.empty()) {
    2627        8389 :         Json::Value cm;
    2628        8389 :         if (json::parse(commit.commit_msg, cm)) {
    2629       32962 :             for (auto const& id : cm.getMemberNames()) {
    2630       24590 :                 if (id == "type") {
    2631        8390 :                     type = cm[id].asString();
    2632        8387 :                     continue;
    2633             :                 }
    2634       16198 :                 message.insert({id, cm[id].asString()});
    2635        8377 :             }
    2636             :         }
    2637        8390 :     }
    2638        8426 :     if (type.empty()) {
    2639           0 :         return std::nullopt;
    2640        8425 :     } else if (type == "application/data-transfer+json") {
    2641             :         // Avoid the client to do the concatenation
    2642         144 :         auto tid = message["tid"];
    2643          72 :         if (not tid.empty()) {
    2644          70 :             message["fileId"] = getFileId(commit.id, tid, message["displayName"]);
    2645             :         } else {
    2646           2 :             message["fileId"] = "";
    2647             :         }
    2648          72 :     }
    2649        8422 :     message["id"] = commit.id;
    2650        8425 :     message["parents"] = parents;
    2651        8424 :     message["linearizedParent"] = commit.linearized_parent;
    2652        8419 :     message["author"] = authorId;
    2653        8419 :     message["type"] = type;
    2654        8419 :     message["timestamp"] = std::to_string(commit.timestamp);
    2655             : 
    2656        8417 :     return message;
    2657        8424 : }
    2658             : 
    2659             : std::string
    2660        2827 : ConversationRepository::Impl::diffStats(const GitDiff& diff) const
    2661             : {
    2662        2827 :     git_diff_stats* stats_ptr = nullptr;
    2663        2827 :     if (git_diff_get_stats(&stats_ptr, diff.get()) < 0) {
    2664           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get diff stats", accountId_, id_);
    2665           0 :         return {};
    2666             :     }
    2667        2827 :     GitDiffStats stats {stats_ptr};
    2668             : 
    2669        2826 :     git_diff_stats_format_t format = GIT_DIFF_STATS_FULL;
    2670        2826 :     git_buf statsBuf = {};
    2671        2826 :     if (git_diff_stats_to_buf(&statsBuf, stats.get(), format, 80) < 0) {
    2672           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to format diff stats", accountId_, id_);
    2673           0 :         return {};
    2674             :     }
    2675             : 
    2676        2826 :     auto res = std::string(statsBuf.ptr, statsBuf.ptr + statsBuf.size);
    2677        2825 :     git_buf_dispose(&statsBuf);
    2678        2827 :     return res;
    2679        2826 : }
    2680             : 
    2681             : ConversationCommit
    2682       11104 : ConversationRepository::Impl::parseCommit(git_repository* repo, const git_commit* commit) const
    2683             : {
    2684             :     git_oid oid;
    2685       11104 :     git_oid_cpy(&oid, git_commit_id(commit));
    2686             : 
    2687       11104 :     ConversationCommit convCommit;
    2688       11100 :     convCommit.id = git_oid_tostr_s(&oid);
    2689       11094 :     convCommit.commit_msg = git_commit_message(commit);
    2690       11097 :     convCommit.timestamp = git_commit_time(commit);
    2691             : 
    2692       11096 :     const git_signature* sig = git_commit_author(commit);
    2693       11097 :     GitAuthor author;
    2694       11098 :     author.name = sig->name;
    2695       11099 :     author.email = sig->email;
    2696       11098 :     convCommit.author = std::move(author);
    2697             : 
    2698       11097 :     std::vector<std::string> parents;
    2699       11100 :     auto parentsCount = git_commit_parentcount(commit);
    2700       20790 :     for (unsigned int p = 0; p < parentsCount; ++p) {
    2701        9683 :         if (const git_oid* pid = git_commit_parent_id(commit, p)) {
    2702        9684 :             parents.emplace_back(git_oid_tostr_s(pid));
    2703             :         }
    2704             :     }
    2705       11107 :     convCommit.parents = std::move(parents);
    2706             : 
    2707       11099 :     git_buf signature = {}, signed_data = {};
    2708       11099 :     if (git_commit_extract_signature(&signature, &signed_data, repo, &oid, "signature") < 0) {
    2709           4 :         JAMI_WARNING("[Account {}] [Conversation {}] Unable to extract signature for commit {}",
    2710             :                      accountId_,
    2711             :                      id_,
    2712             :                      convCommit.id);
    2713             :     } else {
    2714       11105 :         convCommit.signature = base64::decode(std::string_view(signature.ptr, signature.size));
    2715       11102 :         convCommit.signed_content = std::vector<uint8_t>(signed_data.ptr, signed_data.ptr + signed_data.size);
    2716             :     }
    2717       11104 :     git_buf_dispose(&signature);
    2718       11104 :     git_buf_dispose(&signed_data);
    2719             : 
    2720       22195 :     return convCommit;
    2721       11106 : }
    2722             : 
    2723             : //////////////////////////////////
    2724             : 
    2725             : std::unique_ptr<ConversationRepository>
    2726         199 : ConversationRepository::createConversation(const std::shared_ptr<JamiAccount>& account,
    2727             :                                            ConversationMode mode,
    2728             :                                            const std::string& otherMember)
    2729             : {
    2730             :     // Create temporary directory because we are unable to know the first hash for now
    2731         199 :     std::uniform_int_distribution<uint64_t> dist;
    2732         398 :     auto conversationsPath = fileutils::get_data_dir() / account->getAccountID() / "conversations";
    2733         199 :     dhtnet::fileutils::check_dir(conversationsPath);
    2734         398 :     auto tmpPath = conversationsPath / std::to_string(dist(account->rand));
    2735         199 :     if (std::filesystem::is_directory(tmpPath)) {
    2736           0 :         JAMI_ERROR("{} already exists. Abort create conversations", tmpPath);
    2737           0 :         return {};
    2738             :     }
    2739         199 :     if (!dhtnet::fileutils::recursive_mkdir(tmpPath, 0700)) {
    2740           0 :         JAMI_ERROR("An error occurred when creating {}. Abort create conversations.", tmpPath);
    2741           0 :         return {};
    2742             :     }
    2743         199 :     auto repo = create_empty_repository(tmpPath.string());
    2744         199 :     if (!repo) {
    2745           0 :         return {};
    2746             :     }
    2747             : 
    2748             :     // Add initial files
    2749         199 :     if (!add_initial_files(repo, account, mode, otherMember)) {
    2750           0 :         JAMI_ERROR("An error occurred while adding the initial files.");
    2751           0 :         dhtnet::fileutils::removeAll(tmpPath, true);
    2752           0 :         return {};
    2753             :     }
    2754             : 
    2755             :     // Commit changes
    2756         199 :     auto id = initial_commit(repo, account, mode, otherMember);
    2757         199 :     if (id.empty()) {
    2758           0 :         JAMI_ERROR("Unable to create initial commit in {}", tmpPath);
    2759           0 :         dhtnet::fileutils::removeAll(tmpPath, true);
    2760           0 :         return {};
    2761             :     }
    2762             : 
    2763             :     // Move to wanted directory
    2764         199 :     auto newPath = conversationsPath / id;
    2765         199 :     std::error_code ec;
    2766         199 :     std::filesystem::rename(tmpPath, newPath, ec);
    2767         199 :     if (ec) {
    2768           0 :         JAMI_ERROR("Unable to move {} in {}: {}", tmpPath, newPath, ec.message());
    2769           0 :         dhtnet::fileutils::removeAll(tmpPath, true);
    2770           0 :         return {};
    2771             :     }
    2772             : 
    2773         796 :     JAMI_LOG("New conversation initialized in {}", newPath);
    2774             : 
    2775         199 :     return std::make_unique<ConversationRepository>(account, id);
    2776         199 : }
    2777             : 
    2778             : std::pair<std::unique_ptr<ConversationRepository>, std::vector<ConversationCommit>>
    2779         192 : ConversationRepository::cloneConversation(const std::shared_ptr<JamiAccount>& account,
    2780             :                                           const std::string& deviceId,
    2781             :                                           const std::string& conversationId)
    2782             : {
    2783             :     // Verify conversationId is not empty to avoid deleting the entire conversations directory
    2784         192 :     if (conversationId.empty()) {
    2785           0 :         JAMI_ERROR("[Account {}] Clone conversation with empty conversationId", account->getAccountID());
    2786           0 :         return {};
    2787             :     }
    2788             : 
    2789         384 :     auto conversationsPath = fileutils::get_data_dir() / account->getAccountID() / "conversations";
    2790         192 :     dhtnet::fileutils::check_dir(conversationsPath);
    2791         192 :     auto path = conversationsPath / conversationId;
    2792         192 :     auto url = fmt::format("git://{}/{}", deviceId, conversationId);
    2793             : #ifdef LIBJAMI_TEST
    2794         192 :     if (FETCH_FROM_LOCAL_REPOS) {
    2795           0 :         url = fmt::format("file://{}",
    2796           0 :                           (fileutils::get_data_dir() / deviceId / "conversations" / conversationId).string());
    2797             :     }
    2798             : #endif
    2799             : 
    2800             :     git_clone_options clone_options;
    2801         192 :     git_clone_options_init(&clone_options, GIT_CLONE_OPTIONS_VERSION);
    2802         192 :     git_fetch_options_init(&clone_options.fetch_opts, GIT_FETCH_OPTIONS_VERSION);
    2803         192 :     clone_options.fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats, void*) {
    2804             :         // Uncomment to get advancment
    2805             :         // if (stats->received_objects % 500 == 0 || stats->received_objects == stats->total_objects)
    2806             :         //     JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects,
    2807             :         //     stats->received_bytes/1024);
    2808             :         // If a pack is more than 256Mb, it's anormal.
    2809           0 :         if (stats->received_bytes > MAX_FETCH_SIZE) {
    2810           0 :             JAMI_ERROR("Abort fetching repository, the fetch is too big: {} bytes ({}/{})",
    2811             :                        stats->received_bytes,
    2812             :                        stats->received_objects,
    2813             :                        stats->total_objects);
    2814           0 :             return -1;
    2815             :         }
    2816           0 :         return 0;
    2817         192 :     };
    2818             : 
    2819         192 :     if (std::filesystem::is_directory(path)) {
    2820             :         // If a crash occurs during a previous clone, just in case
    2821           0 :         JAMI_WARNING("[Account {}] [Conversation {}] Removing non empty directory {}",
    2822             :                      account->getAccountID(),
    2823             :                      conversationId,
    2824             :                      path);
    2825           0 :         if (dhtnet::fileutils::removeAll(path, true) != 0)
    2826           0 :             return {};
    2827             :     }
    2828             : 
    2829         768 :     JAMI_DEBUG("[Account {}] [Conversation {}] Start clone of {:s} to {}",
    2830             :                account->getAccountID(),
    2831             :                conversationId,
    2832             :                url,
    2833             :                path);
    2834         192 :     git_repository* rep = nullptr;
    2835         192 :     git_clone_options opts = GIT_CLONE_OPTIONS_INIT;
    2836         192 :     opts.fetch_opts.follow_redirects = GIT_REMOTE_REDIRECT_NONE;
    2837         192 :     if (auto err = git_clone(&rep, url.c_str(), path.string().c_str(), &opts)) {
    2838           1 :         if (const git_error* gerr = giterr_last())
    2839           4 :             JAMI_ERROR("[Account {}] [Conversation {}] Error when retrieving remote conversation: {:s} {}",
    2840             :                        account->getAccountID(),
    2841             :                        conversationId,
    2842             :                        gerr->message,
    2843             :                        path);
    2844             :         else
    2845           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unknown error {:d} when retrieving remote conversation",
    2846             :                        account->getAccountID(),
    2847             :                        conversationId,
    2848             :                        err);
    2849           1 :         return {};
    2850             :     }
    2851         191 :     git_repository_free(rep);
    2852         191 :     auto repo = std::make_unique<ConversationRepository>(account, conversationId);
    2853         190 :     repo->pinCertificates(true); // need to load certificates to validate unknown members
    2854         190 :     auto [commitsToValidate, valid] = repo->validClone();
    2855         190 :     if (!valid) {
    2856           4 :         repo->erase();
    2857          16 :         JAMI_ERROR("[Account {}] [Conversation {}] An error occurred while validating remote conversation.",
    2858             :                    account->getAccountID(),
    2859             :                    conversationId);
    2860           4 :         return {};
    2861             :     }
    2862         744 :     JAMI_LOG("[Account {}] [Conversation {}] New conversation cloned in {}",
    2863             :              account->getAccountID(),
    2864             :              conversationId,
    2865             :              path);
    2866         186 :     return {std::move(repo), std::move(commitsToValidate)};
    2867         194 : }
    2868             : 
    2869             : bool
    2870        2114 : ConversationRepository::Impl::validCommits(const std::vector<ConversationCommit>& commitsToValidate) const
    2871             : {
    2872        2114 :     auto repo = repository();
    2873             : 
    2874        4020 :     for (const auto& commit : commitsToValidate) {
    2875        1932 :         auto userDevice = commit.author.email;
    2876        1932 :         auto validUserAtCommit = commit.id;
    2877             : 
    2878             :         git_oid oid;
    2879        1932 :         git_commit* commit_ptr = nullptr;
    2880        1932 :         if (git_oid_fromstr(&oid, validUserAtCommit.c_str()) < 0
    2881        1932 :             || git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
    2882           0 :             JAMI_WARNING("Failed to look up commit {}", validUserAtCommit.c_str());
    2883             :         }
    2884        1932 :         GitBuf sig(new git_buf {});
    2885        1932 :         GitBuf sig_data(new git_buf {});
    2886             : 
    2887             :         // Extract the signature block and signature content from the commit
    2888        1932 :         int sig_extract_res = git_commit_extract_signature(sig.get(), sig_data.get(), repo.get(), &oid, "signature");
    2889        1931 :         if (sig_extract_res != 0) {
    2890           0 :             switch (sig_extract_res) {
    2891           0 :             case GIT_ERROR_INVALID:
    2892           0 :                 JAMI_ERROR("Error, the commit ID ({}) does not correspond to a commit.", validUserAtCommit);
    2893           0 :                 break;
    2894           0 :             case GIT_ERROR_OBJECT:
    2895           0 :                 JAMI_ERROR("Error, the commit ID ({}) does not have a signature.", validUserAtCommit);
    2896           0 :                 break;
    2897           0 :             default:
    2898           0 :                 JAMI_ERROR("An unknown error occurred while extracting signature for commit ID {}.", validUserAtCommit);
    2899           0 :                 break;
    2900             :             }
    2901           0 :             emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2902           0 :                                                                          id_,
    2903             :                                                                          EVALIDFETCH,
    2904             :                                                                          "Malformed commit");
    2905           0 :             return false;
    2906             :         }
    2907             : 
    2908        1931 :         if (commit.parents.size() == 0) {
    2909         188 :             if (!checkInitialCommit(userDevice, commit.id, commit.commit_msg)) {
    2910           8 :                 JAMI_WARNING("[Account {}] [Conversation {}] Malformed initial commit {}. Please "
    2911             :                              "ensure that you are using the latest "
    2912             :                              "version of Jami, or that one of your contacts is not performing any "
    2913             :                              "unwanted actions.",
    2914             :                              accountId_,
    2915             :                              id_,
    2916             :                              commit.id);
    2917           2 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2918           2 :                                                                              id_,
    2919             :                                                                              EVALIDFETCH,
    2920             :                                                                              "Malformed initial commit");
    2921           2 :                 return false;
    2922             :             }
    2923        1743 :         } else if (commit.parents.size() == 1) {
    2924        1733 :             std::string type = {}, editId = {};
    2925        1734 :             Json::Value cm;
    2926        1734 :             if (json::parse(commit.commit_msg, cm)) {
    2927        1734 :                 type = cm["type"].asString();
    2928        1734 :                 editId = cm["edit"].asString();
    2929             :             } else {
    2930           0 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2931           0 :                                                                              id_,
    2932             :                                                                              EVALIDFETCH,
    2933             :                                                                              "Malformed commit");
    2934           0 :                 return false;
    2935             :             }
    2936             : 
    2937        1734 :             if (type == "vote") {
    2938             :                 // Check that vote is valid
    2939           8 :                 if (!checkVote(userDevice, commit.id, commit.parents[0])) {
    2940           4 :                     JAMI_WARNING("[Account {}] [Conversation {}] Malformed vote commit {}. Please "
    2941             :                                  "ensure that you are using the latest "
    2942             :                                  "version of Jami, or that one of your contacts is not performing "
    2943             :                                  "any unwanted actions.",
    2944             :                                  accountId_,
    2945             :                                  id_,
    2946             :                                  commit.id);
    2947             : 
    2948           1 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2949           1 :                                                                                  id_,
    2950             :                                                                                  EVALIDFETCH,
    2951             :                                                                                  "Malformed vote");
    2952           1 :                     return false;
    2953             :                 }
    2954        1726 :             } else if (type == "member") {
    2955        1558 :                 std::string action = cm["action"].asString();
    2956        1558 :                 std::string uriMember = cm["uri"].asString();
    2957             : 
    2958        1558 :                 dht::InfoHash h(uriMember);
    2959        1558 :                 if (not h) {
    2960           8 :                     JAMI_WARNING("[Account {}] [Conversation {}] Commit {} with invalid member URI {}. Please ensure "
    2961             :                                  "that you are using the latest version of Jami, or that one of your contacts is not "
    2962             :                                  "performing any unwanted actions.",
    2963             :                                  accountId_,
    2964             :                                  id_,
    2965             :                                  commit.id,
    2966             :                                  uriMember);
    2967             : 
    2968           2 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2969           2 :                                                                                  id_,
    2970             :                                                                                  EVALIDFETCH,
    2971             :                                                                                  "Invalid member URI");
    2972           2 :                     return false;
    2973             :                 }
    2974        1556 :                 if (action == "add") {
    2975         756 :                     if (!checkValidAdd(userDevice, uriMember, commit.id, commit.parents[0])) {
    2976          12 :                         JAMI_WARNING("[Account {}] [Conversation {}] Malformed add commit {}. Please ensure that you "
    2977             :                                      "are using the latest version of Jami, or that one of your contacts is not "
    2978             :                                      "performing any unwanted actions.",
    2979             :                                      accountId_,
    2980             :                                      id_,
    2981             :                                      commit.id);
    2982             : 
    2983           3 :                         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2984           3 :                                                                                      id_,
    2985             :                                                                                      EVALIDFETCH,
    2986             :                                                                                      "Malformed add member commit");
    2987           3 :                         return false;
    2988             :                     }
    2989         800 :                 } else if (action == "join") {
    2990         790 :                     if (!checkValidJoins(userDevice, uriMember, commit.id, commit.parents[0])) {
    2991          12 :                         JAMI_WARNING("[Account {}] [Conversation {}] Malformed joins commit {}. "
    2992             :                                      "Please ensure that you are using the latest "
    2993             :                                      "version of Jami, or that one of your contacts is not "
    2994             :                                      "performing any unwanted actions.",
    2995             :                                      accountId_,
    2996             :                                      id_,
    2997             :                                      commit.id);
    2998             : 
    2999           3 :                         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3000           3 :                                                                                      id_,
    3001             :                                                                                      EVALIDFETCH,
    3002             :                                                                                      "Malformed join member commit");
    3003           3 :                         return false;
    3004             :                     }
    3005          10 :                 } else if (action == "remove") {
    3006             :                     // In this case, we remove the user. So if self, the user will not be
    3007             :                     // valid for this commit. Check previous commit
    3008           1 :                     validUserAtCommit = commit.parents[0];
    3009           1 :                     if (!checkValidRemove(userDevice, uriMember, commit.id, commit.parents[0])) {
    3010           0 :                         JAMI_WARNING("[Account {}] [Conversation {}] Malformed removes commit {}. "
    3011             :                                      "Please ensure that you are using the latest "
    3012             :                                      "version of Jami, or that one of your contacts is not "
    3013             :                                      "performing any unwanted actions.",
    3014             :                                      accountId_,
    3015             :                                      id_,
    3016             :                                      commit.id);
    3017             : 
    3018           0 :                         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3019           0 :                                                                                      id_,
    3020             :                                                                                      EVALIDFETCH,
    3021             :                                                                                      "Malformed remove member commit");
    3022           0 :                         return false;
    3023             :                     }
    3024           9 :                 } else if (action == "ban" || action == "unban") {
    3025             :                     // Note device.size() == "member".size()
    3026           9 :                     if (!checkValidVoteResolution(userDevice, uriMember, commit.id, commit.parents[0], action)) {
    3027          16 :                         JAMI_WARNING("[Account {}] [Conversation {}] Malformed removes commit {}. "
    3028             :                                      "Please ensure that you are using the latest "
    3029             :                                      "version of Jami, or that one of your contacts is not "
    3030             :                                      "performing any unwanted actions.",
    3031             :                                      accountId_,
    3032             :                                      id_,
    3033             :                                      commit.id);
    3034             : 
    3035           4 :                         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3036           4 :                                                                                      id_,
    3037             :                                                                                      EVALIDFETCH,
    3038             :                                                                                      "Malformed ban member commit");
    3039           4 :                         return false;
    3040             :                     }
    3041             :                 } else {
    3042           0 :                     JAMI_WARNING("[Account {}] [Conversation {}] Malformed member commit {} with "
    3043             :                                  "action {}. Please ensure that you are using the latest "
    3044             :                                  "version of Jami, or that one of your contacts is not performing "
    3045             :                                  "any unwanted actions.",
    3046             :                                  accountId_,
    3047             :                                  id_,
    3048             :                                  commit.id,
    3049             :                                  action);
    3050             : 
    3051           0 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3052           0 :                                                                                  id_,
    3053             :                                                                                  EVALIDFETCH,
    3054             :                                                                                  "Malformed member commit");
    3055           0 :                     return false;
    3056             :                 }
    3057        1738 :             } else if (type == "application/update-profile") {
    3058           7 :                 if (!checkValidProfileUpdate(userDevice, commit.id, commit.parents[0])) {
    3059           8 :                     JAMI_WARNING("[Account {}] [Conversation {}] Malformed profile updates commit "
    3060             :                                  "{}. Please ensure that you are using the latest "
    3061             :                                  "version of Jami, or that one of your contacts is not performing "
    3062             :                                  "any unwanted actions.",
    3063             :                                  accountId_,
    3064             :                                  id_,
    3065             :                                  commit.id);
    3066             : 
    3067           2 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3068           2 :                                                                                  id_,
    3069             :                                                                                  EVALIDFETCH,
    3070             :                                                                                  "Malformed profile updates commit");
    3071           2 :                     return false;
    3072             :                 }
    3073         161 :             } else if (type == "application/edited-message" || !editId.empty()) {
    3074           2 :                 if (!checkEdit(userDevice, commit)) {
    3075           4 :                     JAMI_ERROR("Commit {:s} malformed", commit.id);
    3076             : 
    3077           1 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3078           1 :                                                                                  id_,
    3079             :                                                                                  EVALIDFETCH,
    3080             :                                                                                  "Malformed edit commit");
    3081           1 :                     return false;
    3082             :                 }
    3083             :             } else {
    3084             :                 // Note: accept all mimetype here, as we can have new mimetypes
    3085             :                 // Just avoid to add weird files
    3086             :                 // Check that no weird file is added outside device cert nor removed
    3087         159 :                 if (!checkValidUserDiff(userDevice, commit.id, commit.parents[0])) {
    3088          20 :                     JAMI_WARNING("[Account {}] [Conversation {}] Malformed {} commit {}. Please "
    3089             :                                  "ensure that you are using the latest "
    3090             :                                  "version of Jami, or that one of your contacts is not performing "
    3091             :                                  "any unwanted actions.",
    3092             :                                  accountId_,
    3093             :                                  id_,
    3094             :                                  type,
    3095             :                                  commit.id);
    3096             : 
    3097           5 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3098           5 :                                                                                  id_,
    3099             :                                                                                  EVALIDFETCH,
    3100             :                                                                                  "Malformed commit");
    3101           5 :                     return false;
    3102             :                 }
    3103             :             }
    3104             :             // For all commits, check that the user is valid.
    3105             :             // So, the user certificate MUST be in /members or /admins
    3106             :             // and device cert MUST be in /devices
    3107        1713 :             if (!isValidUserAtCommit(userDevice, validUserAtCommit, *sig, *sig_data)) {
    3108           8 :                 JAMI_WARNING("[Account {}] [Conversation {}] Malformed commit {}. Please ensure "
    3109             :                              "that you are using the latest "
    3110             :                              "version of Jami, or that one of your contacts is not performing any "
    3111             :                              "unwanted actions. {}",
    3112             :                              accountId_,
    3113             :                              id_,
    3114             :                              validUserAtCommit,
    3115             :                              commit.commit_msg);
    3116           2 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3117           2 :                                                                              id_,
    3118             :                                                                              EVALIDFETCH,
    3119             :                                                                              "Invalid user");
    3120           2 :                 return false;
    3121             :             }
    3122        1780 :         } else {
    3123             :             // For all commits, check that the user is valid.
    3124             :             // So, the user certificate MUST be in /members or /admins
    3125             :             // and device cert MUST be in /devices
    3126          10 :             if (!isValidUserAtCommit(userDevice, validUserAtCommit, *sig, *sig_data)) {
    3127           0 :                 JAMI_WARNING("[Account {}] [Conversation {}] Malformed commit {}.Please ensure "
    3128             :                              "that you are using the latest "
    3129             :                              "version of Jami, or that one of your contacts is not performing any "
    3130             :                              "unwanted actions. {}",
    3131             :                              accountId_,
    3132             :                              id_,
    3133             :                              validUserAtCommit,
    3134             :                              commit.commit_msg);
    3135           0 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3136           0 :                                                                              id_,
    3137             :                                                                              EVALIDFETCH,
    3138             :                                                                              "Malformed commit");
    3139           0 :                 return false;
    3140             :             }
    3141             : 
    3142          10 :             if (!checkValidMergeCommit(commit.id, commit.parents)) {
    3143           4 :                 JAMI_WARNING("[Account {}] [Conversation {}] Malformed merge commit {}. Please "
    3144             :                              "ensure that you are using the latest "
    3145             :                              "version of Jami, or that one of your contacts is not performing "
    3146             :                              "any unwanted actions.",
    3147             :                              accountId_,
    3148             :                              id_,
    3149             :                              commit.id);
    3150             : 
    3151           1 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3152           1 :                                                                              id_,
    3153             :                                                                              EVALIDFETCH,
    3154             :                                                                              "Malformed merge commit");
    3155           1 :                 return false;
    3156             :             }
    3157             :         }
    3158        7624 :         JAMI_DEBUG("[Account {}] [Conversation {}] Validate commit {}", accountId_, id_, commit.id);
    3159        2010 :     }
    3160        2088 :     return true;
    3161        2114 : }
    3162             : 
    3163             : /////////////////////////////////////////////////////////////////////////////////
    3164             : 
    3165         427 : ConversationRepository::ConversationRepository(const std::shared_ptr<JamiAccount>& account, const std::string& id)
    3166         427 :     : pimpl_ {new Impl {account, id}}
    3167         426 : {}
    3168             : 
    3169         425 : ConversationRepository::~ConversationRepository() = default;
    3170             : 
    3171             : const std::string&
    3172       15805 : ConversationRepository::id() const
    3173             : {
    3174       15805 :     return pimpl_->id_;
    3175             : }
    3176             : 
    3177             : std::string
    3178         135 : ConversationRepository::addMember(const std::string& uri)
    3179             : {
    3180         135 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3181         135 :     pimpl_->resetHard();
    3182         135 :     auto repo = pimpl_->repository();
    3183         135 :     if (not repo)
    3184           0 :         return {};
    3185             : 
    3186             :     // First, we need to add the member file to the repository if not present
    3187         135 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3188             : 
    3189         135 :     std::filesystem::path invitedPath = repoPath / MemberPath::INVITED;
    3190         135 :     if (!dhtnet::fileutils::recursive_mkdir(invitedPath, 0700)) {
    3191           0 :         JAMI_ERROR("Error when creating {}.", invitedPath);
    3192           0 :         return {};
    3193             :     }
    3194         135 :     std::filesystem::path devicePath = invitedPath / uri;
    3195         135 :     if (std::filesystem::is_regular_file(devicePath)) {
    3196           0 :         JAMI_WARNING("Member {} already present!", uri);
    3197           0 :         return {};
    3198             :     }
    3199             : 
    3200         135 :     std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
    3201         135 :     if (!file.is_open()) {
    3202           0 :         JAMI_ERROR("Unable to write data to {}", devicePath);
    3203           0 :         return {};
    3204             :     }
    3205         135 :     std::string path = "invited/" + uri;
    3206         135 :     if (!pimpl_->add(path))
    3207           0 :         return {};
    3208             : 
    3209         135 :     Json::Value json;
    3210         135 :     json["action"] = "add";
    3211         135 :     json["uri"] = uri;
    3212         135 :     json["type"] = "member";
    3213         135 :     auto commitId = pimpl_->commit(json::toString(json));
    3214         135 :     if (commitId.empty()) {
    3215           0 :         JAMI_ERROR("Unable to commit addition of member {}", uri);
    3216           0 :         return {};
    3217             :     }
    3218             : 
    3219         135 :     std::lock_guard lk(pimpl_->membersMtx_);
    3220         135 :     pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::INVITED});
    3221         135 :     pimpl_->saveMembers();
    3222         135 :     return commitId;
    3223         135 : }
    3224             : 
    3225             : void
    3226         390 : ConversationRepository::onMembersChanged(OnMembersChanged&& cb)
    3227             : {
    3228         390 :     pimpl_->onMembersChanged_ = std::move(cb);
    3229         390 : }
    3230             : 
    3231             : std::string
    3232           1 : ConversationRepository::amend(const std::string& id, const std::string& msg)
    3233             : {
    3234           1 :     GitSignature sig = pimpl_->signature();
    3235           1 :     if (!sig)
    3236           0 :         return {};
    3237             : 
    3238             :     git_oid tree_id, commit_id;
    3239           1 :     git_commit* commit_ptr = nullptr;
    3240           1 :     auto repo = pimpl_->repository();
    3241           1 :     if (!repo || git_oid_fromstr(&tree_id, id.c_str()) < 0 || git_commit_lookup(&commit_ptr, repo.get(), &tree_id) < 0) {
    3242           0 :         GitCommit commit {commit_ptr};
    3243           0 :         JAMI_WARNING("Failed to look up commit {}", id);
    3244           0 :         return {};
    3245           0 :     }
    3246           1 :     GitCommit commit {commit_ptr};
    3247             : 
    3248           1 :     if (git_commit_amend(&commit_id, commit.get(), nullptr, sig.get(), sig.get(), nullptr, msg.c_str(), nullptr) < 0) {
    3249           0 :         if (const git_error* err = giterr_last())
    3250           0 :             JAMI_ERROR("Unable to amend commit: {}", err->message);
    3251           0 :         return {};
    3252             :     }
    3253             : 
    3254             :     // Move commit to main branch
    3255           1 :     git_reference* ref_ptr = nullptr;
    3256           1 :     if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_id, true, nullptr) < 0) {
    3257           0 :         if (const git_error* err = giterr_last()) {
    3258           0 :             JAMI_ERROR("Unable to move commit to main: {}", err->message);
    3259           0 :             emitSignal<libjami::ConversationSignal::OnConversationError>(pimpl_->accountId_,
    3260           0 :                                                                          pimpl_->id_,
    3261             :                                                                          ECOMMIT,
    3262           0 :                                                                          err->message);
    3263             :         }
    3264           0 :         return {};
    3265             :     }
    3266           1 :     git_reference_free(ref_ptr);
    3267             : 
    3268           1 :     auto commit_str = git_oid_tostr_s(&commit_id);
    3269           1 :     if (commit_str) {
    3270           4 :         JAMI_DEBUG("Commit {} amended (new ID: {})", id, commit_str);
    3271           1 :         return commit_str;
    3272             :     }
    3273           0 :     return {};
    3274           1 : }
    3275             : 
    3276             : bool
    3277        1952 : ConversationRepository::fetch(const std::string& remoteDeviceId)
    3278             : {
    3279        1952 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3280        1952 :     pimpl_->resetHard();
    3281             :     // Fetch distant repository
    3282        1952 :     git_remote* remote_ptr = nullptr;
    3283             :     git_fetch_options fetch_opts;
    3284        1952 :     git_fetch_options_init(&fetch_opts, GIT_FETCH_OPTIONS_VERSION);
    3285        1952 :     fetch_opts.follow_redirects = GIT_REMOTE_REDIRECT_NONE;
    3286             : 
    3287             :     // Assert that repository exists
    3288        1952 :     auto repo = pimpl_->repository();
    3289        1952 :     if (!repo)
    3290           0 :         return false;
    3291        1952 :     auto res = git_remote_lookup(&remote_ptr, repo.get(), remoteDeviceId.c_str());
    3292        1952 :     if (res != 0) {
    3293         730 :         if (res != GIT_ENOTFOUND) {
    3294           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up for remote {}",
    3295             :                        pimpl_->accountId_,
    3296             :                        pimpl_->id_,
    3297             :                        remoteDeviceId);
    3298           0 :             return false;
    3299             :         }
    3300         730 :         std::string channelName = fmt::format("git://{}/{}", remoteDeviceId, pimpl_->id_);
    3301             : #ifdef LIBJAMI_TEST
    3302         730 :         if (FETCH_FROM_LOCAL_REPOS) {
    3303             :             channelName
    3304           0 :                 = fmt::format("file://{}",
    3305           0 :                               (fileutils::get_data_dir() / remoteDeviceId / "conversations" / pimpl_->id_).string());
    3306             :         }
    3307             : #endif
    3308         730 :         if (git_remote_create(&remote_ptr, repo.get(), remoteDeviceId.c_str(), channelName.c_str()) < 0) {
    3309           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to create remote for repository",
    3310             :                        pimpl_->accountId_,
    3311             :                        pimpl_->id_);
    3312           0 :             return false;
    3313             :         }
    3314         730 :     }
    3315        1952 :     GitRemote remote {remote_ptr};
    3316             : 
    3317       33944 :     fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats, void*) {
    3318             :         // Uncomment to get advancment
    3319             :         // if (stats->received_objects % 500 == 0 || stats->received_objects == stats->total_objects)
    3320             :         //     JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects,
    3321             :         //     stats->received_bytes/1024);
    3322             :         // If a pack is more than 256Mb, it's anormal.
    3323       31992 :         if (stats->received_bytes > MAX_FETCH_SIZE) {
    3324           0 :             JAMI_ERROR("Abort fetching repository, the fetch is too big: {} bytes ({}/{})",
    3325             :                        stats->received_bytes,
    3326             :                        stats->received_objects,
    3327             :                        stats->total_objects);
    3328           0 :             return -1;
    3329             :         }
    3330       31992 :         return 0;
    3331        1952 :     };
    3332        1952 :     if (git_remote_fetch(remote.get(), nullptr, &fetch_opts, "fetch") < 0) {
    3333          33 :         const git_error* err = giterr_last();
    3334          33 :         if (err) {
    3335         132 :             JAMI_WARNING("[Account {}] [Conversation {}] Unable to fetch remote repository: {:s}",
    3336             :                          pimpl_->accountId_,
    3337             :                          pimpl_->id_,
    3338             :                          err->message);
    3339             :         }
    3340          33 :         return false;
    3341             :     }
    3342             : 
    3343        1919 :     return true;
    3344        1952 : }
    3345             : 
    3346             : std::vector<std::map<std::string, std::string>>
    3347        1917 : ConversationRepository::mergeHistory(const std::string& uri,
    3348             :                                      std::function<void(const std::string&)>&& disconnectFromPeerCb)
    3349             : {
    3350        3836 :     auto remoteHeadRes = remoteHead(uri);
    3351        1919 :     if (remoteHeadRes.empty()) {
    3352           0 :         JAMI_WARNING("[Account {}] [Conversation {}] Unable to get HEAD of {}", pimpl_->accountId_, pimpl_->id_, uri);
    3353           0 :         return {};
    3354             :     }
    3355             : 
    3356             :     // Validate commit
    3357        1919 :     auto [newCommits, err] = validFetch(uri);
    3358        1919 :     if (newCommits.empty()) {
    3359        1010 :         if (err)
    3360          68 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to validate history with {}",
    3361             :                        pimpl_->accountId_,
    3362             :                        pimpl_->id_,
    3363             :                        uri);
    3364        1010 :         removeBranchWith(uri);
    3365        1010 :         return {};
    3366             :     }
    3367             : 
    3368             :     // If validated, merge
    3369         909 :     auto [ok, cid] = merge(remoteHeadRes);
    3370         908 :     if (!ok) {
    3371           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to merge history with {}",
    3372             :                    pimpl_->accountId_,
    3373             :                    pimpl_->id_,
    3374             :                    uri);
    3375           0 :         removeBranchWith(uri);
    3376           0 :         return {};
    3377             :     }
    3378         908 :     if (!cid.empty()) {
    3379             :         // A merge commit was generated, should be added in new commits
    3380          18 :         auto commit = getCommit(cid);
    3381          18 :         if (commit != std::nullopt)
    3382          18 :             newCommits.emplace_back(*commit);
    3383          18 :     }
    3384             : 
    3385        3633 :     JAMI_LOG("[Account {}] [Conversation {}] Successfully merged history with {}", pimpl_->accountId_, pimpl_->id_, uri);
    3386         909 :     auto result = convCommitsToMap(newCommits);
    3387        1850 :     for (auto& commit : result) {
    3388         941 :         auto it = commit.find("type");
    3389         941 :         if (it != commit.end() && it->second == "member") {
    3390         773 :             refreshMembers();
    3391             : 
    3392         773 :             if (commit["action"] == "ban")
    3393           5 :                 disconnectFromPeerCb(commit["uri"]);
    3394             :         }
    3395             :     }
    3396         909 :     return result;
    3397        1919 : }
    3398             : 
    3399             : std::string
    3400        3837 : ConversationRepository::remoteHead(const std::string& remoteDeviceId, const std::string& branch) const
    3401             : {
    3402        3837 :     git_remote* remote_ptr = nullptr;
    3403        3837 :     auto repo = pimpl_->repository();
    3404        3837 :     if (!repo || git_remote_lookup(&remote_ptr, repo.get(), remoteDeviceId.c_str()) < 0) {
    3405           0 :         JAMI_WARNING("No remote found with ID: {}", remoteDeviceId);
    3406           0 :         return {};
    3407             :     }
    3408        3838 :     GitRemote remote {remote_ptr};
    3409             : 
    3410        3838 :     git_reference* head_ref_ptr = nullptr;
    3411        7675 :     std::string remoteHead = "refs/remotes/" + remoteDeviceId + "/" + branch;
    3412             :     git_oid commit_id;
    3413        3838 :     if (git_reference_name_to_id(&commit_id, repo.get(), remoteHead.c_str()) < 0) {
    3414           0 :         const git_error* err = giterr_last();
    3415           0 :         if (err)
    3416           0 :             JAMI_ERROR("failed to look up {} ref: {}", remoteHead, err->message);
    3417           0 :         return {};
    3418             :     }
    3419        3838 :     GitReference head_ref {head_ref_ptr};
    3420             : 
    3421        3838 :     auto commit_str = git_oid_tostr_s(&commit_id);
    3422        3838 :     if (!commit_str)
    3423           0 :         return {};
    3424        3838 :     return commit_str;
    3425        3838 : }
    3426             : 
    3427             : void
    3428         352 : ConversationRepository::Impl::addUserDevice()
    3429             : {
    3430         352 :     auto account = account_.lock();
    3431         352 :     if (!account)
    3432           0 :         return;
    3433             : 
    3434             :     // First, we need to add device file to the repository if not present
    3435         352 :     auto repo = repository();
    3436         352 :     if (!repo)
    3437           0 :         return;
    3438             :     // NOTE: libgit2 uses / for files
    3439         352 :     std::string path = fmt::format("devices/{}.crt", deviceId_);
    3440         352 :     std::filesystem::path devicePath = git_repository_workdir(repo.get()) + path;
    3441         352 :     if (!std::filesystem::is_regular_file(devicePath)) {
    3442         162 :         std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
    3443         162 :         if (!file.is_open()) {
    3444           0 :             JAMI_ERROR("Unable to write data to {}", devicePath);
    3445           0 :             return;
    3446             :         }
    3447         162 :         auto cert = account->identity().second;
    3448         162 :         auto deviceCert = cert->toString(false);
    3449         162 :         file << deviceCert;
    3450         162 :         file.close();
    3451             : 
    3452         162 :         if (!add(path))
    3453           0 :             JAMI_WARNING("Unable to add file {}", devicePath);
    3454         162 :     }
    3455         352 : }
    3456             : 
    3457             : void
    3458        3390 : ConversationRepository::Impl::resetHard()
    3459             : {
    3460             : #ifdef LIBJAMI_TEST
    3461        3390 :     if (DISABLE_RESET)
    3462         469 :         return;
    3463             : #endif
    3464        2921 :     auto repo = repository();
    3465        2921 :     if (!repo)
    3466           0 :         return;
    3467        2921 :     git_object* head_commit_obj = nullptr;
    3468        2921 :     auto error = git_revparse_single(&head_commit_obj, repo.get(), "HEAD");
    3469        2921 :     if (error < 0) {
    3470           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get HEAD commit: {}", accountId_, id_, error);
    3471           0 :         return;
    3472             :     }
    3473        2921 :     GitObject target {head_commit_obj};
    3474        2920 :     git_reset(repo.get(), head_commit_obj, GIT_RESET_HARD, nullptr);
    3475        2921 : }
    3476             : 
    3477             : std::string
    3478         161 : ConversationRepository::commitMessage(const std::string& msg, bool verifyDevice)
    3479             : {
    3480         161 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3481         161 :     pimpl_->resetHard();
    3482         322 :     return pimpl_->commitMessage(msg, verifyDevice);
    3483         161 : }
    3484             : 
    3485             : std::string
    3486         352 : ConversationRepository::Impl::commitMessage(const std::string& msg, bool verifyDevice)
    3487             : {
    3488         352 :     addUserDevice();
    3489         352 :     return commit(msg, verifyDevice);
    3490             : }
    3491             : 
    3492             : std::vector<std::string>
    3493           0 : ConversationRepository::commitMessages(const std::vector<std::string>& msgs)
    3494             : {
    3495           0 :     pimpl_->addUserDevice();
    3496           0 :     std::vector<std::string> ret;
    3497           0 :     ret.reserve(msgs.size());
    3498           0 :     for (const auto& msg : msgs)
    3499           0 :         ret.emplace_back(pimpl_->commit(msg));
    3500           0 :     return ret;
    3501           0 : }
    3502             : 
    3503             : std::vector<ConversationCommit>
    3504         202 : ConversationRepository::log(const LogOptions& options) const
    3505             : {
    3506         202 :     return pimpl_->log(options);
    3507             : }
    3508             : 
    3509             : void
    3510        1559 : ConversationRepository::log(PreConditionCb&& preCondition,
    3511             :                             std::function<void(ConversationCommit&&)>&& emplaceCb,
    3512             :                             PostConditionCb&& postCondition,
    3513             :                             const std::string& from,
    3514             :                             bool logIfNotFound) const
    3515             : {
    3516        1559 :     pimpl_->forEachCommit(std::move(preCondition), std::move(emplaceCb), std::move(postCondition), from, logIfNotFound);
    3517        1559 : }
    3518             : 
    3519             : bool
    3520       13835 : ConversationRepository::hasCommit(const std::string& commitId) const
    3521             : {
    3522       13835 :     return pimpl_->hasCommit(commitId);
    3523             : }
    3524             : 
    3525             : std::optional<ConversationCommit>
    3526         362 : ConversationRepository::getCommit(const std::string& commitId) const
    3527             : {
    3528         362 :     return pimpl_->getCommit(commitId);
    3529             : }
    3530             : 
    3531             : std::pair<bool, std::string>
    3532         912 : ConversationRepository::merge(const std::string& merge_id, bool force)
    3533             : {
    3534         912 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3535         912 :     pimpl_->resetHard();
    3536             :     // First, the repository must be in a clean state
    3537         912 :     auto repo = pimpl_->repository();
    3538         912 :     if (!repo) {
    3539           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to merge without repo", pimpl_->accountId_, pimpl_->id_);
    3540           0 :         return {false, ""};
    3541             :     }
    3542         912 :     int state = git_repository_state(repo.get());
    3543         912 :     if (state != GIT_REPOSITORY_STATE_NONE) {
    3544           0 :         pimpl_->resetHard();
    3545           0 :         int state = git_repository_state(repo.get());
    3546           0 :         if (state != GIT_REPOSITORY_STATE_NONE) {
    3547           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: repository is in unexpected state {}",
    3548             :                        pimpl_->accountId_,
    3549             :                        pimpl_->id_,
    3550             :                        state);
    3551           0 :             return {false, ""};
    3552             :         }
    3553             :     }
    3554             :     // Checkout main (to do a `git_merge branch`)
    3555         912 :     if (git_repository_set_head(repo.get(), "refs/heads/main") < 0) {
    3556           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to checkout main branch",
    3557             :                    pimpl_->accountId_,
    3558             :                    pimpl_->id_);
    3559           0 :         return {false, ""};
    3560             :     }
    3561             : 
    3562             :     // Then check that merge_id exists
    3563             :     git_oid commit_id;
    3564         912 :     if (git_oid_fromstr(&commit_id, merge_id.c_str()) < 0) {
    3565           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to look up commit {}",
    3566             :                    pimpl_->accountId_,
    3567             :                    pimpl_->id_,
    3568             :                    merge_id);
    3569           0 :         return {false, ""};
    3570             :     }
    3571         912 :     git_annotated_commit* annotated_ptr = nullptr;
    3572         912 :     if (git_annotated_commit_lookup(&annotated_ptr, repo.get(), &commit_id) < 0) {
    3573           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to look up commit {}",
    3574             :                    pimpl_->accountId_,
    3575             :                    pimpl_->id_,
    3576             :                    merge_id);
    3577           0 :         return {false, ""};
    3578             :     }
    3579         912 :     GitAnnotatedCommit annotated {annotated_ptr};
    3580             : 
    3581             :     // Now, we can analyze the type of merge required
    3582             :     git_merge_analysis_t analysis;
    3583             :     git_merge_preference_t preference;
    3584         912 :     const git_annotated_commit* const_annotated = annotated.get();
    3585         912 :     if (git_merge_analysis(&analysis, &preference, repo.get(), &const_annotated, 1) < 0) {
    3586           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: repository analysis failed",
    3587             :                    pimpl_->accountId_,
    3588             :                    pimpl_->id_);
    3589           0 :         return {false, ""};
    3590             :     }
    3591             : 
    3592             :     // Handle easy merges
    3593         912 :     if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
    3594           0 :         JAMI_LOG("Already up-to-date");
    3595           0 :         return {true, ""};
    3596         912 :     } else if (analysis & GIT_MERGE_ANALYSIS_UNBORN
    3597         912 :                || (analysis & GIT_MERGE_ANALYSIS_FASTFORWARD && !(preference & GIT_MERGE_PREFERENCE_NO_FASTFORWARD))) {
    3598         892 :         if (analysis & GIT_MERGE_ANALYSIS_UNBORN)
    3599           0 :             JAMI_LOG("[Account {}] [Conversation {}] Merge analysis result: Unborn", pimpl_->accountId_, pimpl_->id_);
    3600             :         else
    3601        3568 :             JAMI_LOG("[Account {}] [Conversation {}] Merge analysis result: Fast-forward",
    3602             :                      pimpl_->accountId_,
    3603             :                      pimpl_->id_);
    3604         892 :         const auto* target_oid = git_annotated_commit_id(annotated.get());
    3605             : 
    3606         892 :         if (!pimpl_->mergeFastforward(target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN))) {
    3607           0 :             const git_error* err = giterr_last();
    3608           0 :             if (err)
    3609           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] Fast forward merge failed: {}",
    3610             :                            pimpl_->accountId_,
    3611             :                            pimpl_->id_,
    3612             :                            err->message);
    3613           0 :             return {false, ""};
    3614             :         }
    3615         892 :         return {true, ""}; // fast forward so no commit generated;
    3616             :     }
    3617             : 
    3618          20 :     if (!pimpl_->validateDevice() && !force) {
    3619           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Invalid device. Not migrated?", pimpl_->accountId_, pimpl_->id_);
    3620           0 :         return {false, ""};
    3621             :     }
    3622             : 
    3623             :     // Else we want to check for conflicts
    3624             :     git_oid head_commit_id;
    3625          20 :     if (git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD") < 0) {
    3626           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", pimpl_->accountId_, pimpl_->id_);
    3627           0 :         return {false, ""};
    3628             :     }
    3629             : 
    3630          20 :     git_commit* head_ptr = nullptr;
    3631          20 :     if (git_commit_lookup(&head_ptr, repo.get(), &head_commit_id) < 0) {
    3632           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", pimpl_->accountId_, pimpl_->id_);
    3633           0 :         return {false, ""};
    3634             :     }
    3635          20 :     GitCommit head_commit {head_ptr};
    3636             : 
    3637          20 :     git_commit* other__ptr = nullptr;
    3638          20 :     if (git_commit_lookup(&other__ptr, repo.get(), &commit_id) < 0) {
    3639           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", pimpl_->accountId_, pimpl_->id_);
    3640           0 :         return {false, ""};
    3641             :     }
    3642          20 :     GitCommit other_commit {other__ptr};
    3643             : 
    3644             :     git_merge_options merge_opts;
    3645          20 :     git_merge_options_init(&merge_opts, GIT_MERGE_OPTIONS_VERSION);
    3646          20 :     merge_opts.recursion_limit = 2;
    3647          20 :     git_index* index_ptr = nullptr;
    3648          20 :     if (git_merge_commits(&index_ptr, repo.get(), head_commit.get(), other_commit.get(), &merge_opts) < 0) {
    3649           0 :         const git_error* err = giterr_last();
    3650           0 :         if (err)
    3651           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Git merge failed: {}",
    3652             :                        pimpl_->accountId_,
    3653             :                        pimpl_->id_,
    3654             :                        err->message);
    3655           0 :         return {false, ""};
    3656             :     }
    3657          20 :     GitIndex index {index_ptr};
    3658          20 :     if (git_index_has_conflicts(index.get())) {
    3659           4 :         JAMI_LOG("Some conflicts were detected during the merge operations. Resolution phase.");
    3660           1 :         if (!pimpl_->resolveConflicts(index.get(), merge_id) or !git_add_all(repo.get())) {
    3661           0 :             JAMI_ERROR("Merge operation aborted; Unable to automatically resolve conflicts");
    3662           0 :             return {false, ""};
    3663             :         }
    3664             :     }
    3665          20 :     auto result = pimpl_->createMergeCommit(index.get(), merge_id);
    3666          80 :     JAMI_LOG("Merge done between {} and main", merge_id);
    3667             : 
    3668          20 :     return {!result.empty(), result};
    3669         912 : }
    3670             : 
    3671             : std::string
    3672         911 : ConversationRepository::diffStats(const std::string& newId, const std::string& oldId) const
    3673             : {
    3674         911 :     return pimpl_->diffStats(newId, oldId);
    3675             : }
    3676             : 
    3677             : std::vector<std::string>
    3678        2827 : ConversationRepository::changedFiles(std::string_view diffStats)
    3679             : {
    3680        2827 :     static const std::regex re(" +\\| +[0-9]+.*");
    3681        2827 :     std::vector<std::string> changedFiles;
    3682        2827 :     std::string_view line;
    3683       10971 :     while (jami::getline(diffStats, line)) {
    3684        8145 :         std::svmatch match;
    3685        8145 :         if (!std::regex_search(line, match, re) && match.size() == 0)
    3686        2827 :             continue;
    3687        5317 :         changedFiles.emplace_back(std::regex_replace(std::string {line}, re, "").substr(1));
    3688        8143 :     }
    3689        5654 :     return changedFiles;
    3690           0 : }
    3691             : 
    3692             : std::string
    3693         187 : ConversationRepository::join()
    3694             : {
    3695         187 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3696         187 :     pimpl_->resetHard();
    3697             :     // Check that not already member
    3698         187 :     auto repo = pimpl_->repository();
    3699         187 :     if (!repo)
    3700           0 :         return {};
    3701         187 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3702         187 :     auto account = pimpl_->account_.lock();
    3703         187 :     if (!account)
    3704           0 :         return {};
    3705         187 :     auto cert = account->identity().second;
    3706         187 :     auto parentCert = cert->issuer;
    3707         187 :     if (!parentCert) {
    3708           0 :         JAMI_ERROR("Parent cert is null!");
    3709           0 :         return {};
    3710             :     }
    3711         187 :     auto uri = parentCert->getId().toString();
    3712         187 :     auto membersPath = repoPath / MemberPath::MEMBERS;
    3713         374 :     auto memberFile = membersPath / (uri + ".crt");
    3714         374 :     auto adminsPath = repoPath / MemberPath::ADMINS / (uri + ".crt");
    3715         187 :     if (std::filesystem::is_regular_file(memberFile) or std::filesystem::is_regular_file(adminsPath)) {
    3716             :         // Already member, nothing to commit
    3717          30 :         return {};
    3718             :     }
    3719             :     // Remove invited/uri.crt
    3720         157 :     auto invitedPath = repoPath / MemberPath::INVITED;
    3721         157 :     dhtnet::fileutils::remove(fileutils::getFullPath(invitedPath, uri));
    3722             :     // Add members/uri.crt
    3723         157 :     if (!dhtnet::fileutils::recursive_mkdir(membersPath, 0700)) {
    3724           0 :         JAMI_ERROR("Error when creating {}. Abort create conversations", membersPath);
    3725           0 :         return {};
    3726             :     }
    3727         157 :     std::ofstream file(memberFile, std::ios::trunc | std::ios::binary);
    3728         157 :     if (!file.is_open()) {
    3729           0 :         JAMI_ERROR("Unable to write data to {}", memberFile);
    3730           0 :         return {};
    3731             :     }
    3732         157 :     file << parentCert->toString(true);
    3733         157 :     file.close();
    3734             :     // git add -A
    3735         157 :     if (!git_add_all(repo.get())) {
    3736           0 :         return {};
    3737             :     }
    3738         157 :     Json::Value json;
    3739         157 :     json["action"] = "join";
    3740         157 :     json["uri"] = uri;
    3741         157 :     json["type"] = "member";
    3742             : 
    3743             :     {
    3744         157 :         std::lock_guard lk(pimpl_->membersMtx_);
    3745         157 :         auto updated = false;
    3746             : 
    3747         633 :         for (auto& member : pimpl_->members_) {
    3748         633 :             if (member.uri == uri) {
    3749         157 :                 updated = true;
    3750         157 :                 member.role = MemberRole::MEMBER;
    3751         157 :                 break;
    3752             :             }
    3753             :         }
    3754         157 :         if (!updated)
    3755           0 :             pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::MEMBER});
    3756         157 :         pimpl_->saveMembers();
    3757         157 :     }
    3758             : 
    3759         314 :     return pimpl_->commitMessage(json::toString(json));
    3760         187 : }
    3761             : 
    3762             : std::string
    3763           7 : ConversationRepository::leave()
    3764             : {
    3765           7 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3766           7 :     pimpl_->resetHard();
    3767             :     // TODO: simplify
    3768           7 :     auto account = pimpl_->account_.lock();
    3769           7 :     auto repo = pimpl_->repository();
    3770           7 :     if (!account || !repo)
    3771           0 :         return {};
    3772             : 
    3773             :     // Remove related files
    3774           7 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3775           7 :     auto crt = fmt::format("{}.crt", pimpl_->userId_);
    3776          14 :     auto adminFile = repoPath / MemberPath::ADMINS / crt;
    3777          14 :     auto memberFile = repoPath / MemberPath::MEMBERS / crt;
    3778           7 :     auto crlsPath = repoPath / "CRLs";
    3779           7 :     std::error_code ec;
    3780             : 
    3781           7 :     if (std::filesystem::is_regular_file(adminFile, ec)) {
    3782           7 :         std::filesystem::remove(adminFile, ec);
    3783             :     }
    3784             : 
    3785           7 :     if (std::filesystem::is_regular_file(memberFile, ec)) {
    3786           0 :         std::filesystem::remove(memberFile, ec);
    3787             :     }
    3788             : 
    3789             :     // /CRLs
    3790           7 :     for (const auto& crl : account->identity().second->getRevocationLists()) {
    3791           0 :         if (!crl)
    3792           0 :             continue;
    3793           0 :         auto crlPath = crlsPath / pimpl_->deviceId_ / fmt::format("{}.crl", dht::toHex(crl->getNumber()));
    3794           0 :         if (std::filesystem::is_regular_file(crlPath, ec)) {
    3795           0 :             std::filesystem::remove(crlPath, ec);
    3796             :         }
    3797           7 :     }
    3798             : 
    3799             :     // Devices
    3800          29 :     for (const auto& certificate : std::filesystem::directory_iterator(repoPath / "devices", ec)) {
    3801           8 :         if (certificate.is_regular_file(ec)) {
    3802             :             try {
    3803          16 :                 crypto::Certificate cert(fileutils::loadFile(certificate.path()));
    3804           8 :                 if (cert.getIssuerUID() == pimpl_->userId_)
    3805           7 :                     std::filesystem::remove(certificate.path(), ec);
    3806           8 :             } catch (...) {
    3807           0 :                 continue;
    3808           0 :             }
    3809             :         }
    3810           7 :     }
    3811             : 
    3812           7 :     if (!git_add_all(repo.get())) {
    3813           0 :         return {};
    3814             :     }
    3815             : 
    3816           7 :     Json::Value json;
    3817           7 :     json["action"] = "remove";
    3818           7 :     json["uri"] = pimpl_->userId_;
    3819           7 :     json["type"] = "member";
    3820             : 
    3821             :     {
    3822           7 :         std::lock_guard lk(pimpl_->membersMtx_);
    3823          21 :         pimpl_->members_.erase(std::remove_if(pimpl_->members_.begin(),
    3824           7 :                                               pimpl_->members_.end(),
    3825           9 :                                               [&](auto& member) { return member.uri == pimpl_->userId_; }),
    3826           7 :                                pimpl_->members_.end());
    3827           7 :         pimpl_->saveMembers();
    3828           7 :     }
    3829             : 
    3830          14 :     return pimpl_->commit(json::toString(json), false);
    3831           7 : }
    3832             : 
    3833             : void
    3834          25 : ConversationRepository::erase()
    3835             : {
    3836             :     // First, we need to add the member file to the repository if not present
    3837          25 :     if (auto repo = pimpl_->repository()) {
    3838          25 :         std::string repoPath = git_repository_workdir(repo.get());
    3839         100 :         JAMI_LOG("Erasing {}", repoPath);
    3840          25 :         dhtnet::fileutils::removeAll(repoPath, true);
    3841          50 :     }
    3842          25 : }
    3843             : 
    3844             : ConversationMode
    3845        5205 : ConversationRepository::mode() const
    3846             : {
    3847        5205 :     return pimpl_->mode();
    3848             : }
    3849             : 
    3850             : std::string
    3851          13 : ConversationRepository::voteKick(const std::string& uri, const std::string& type)
    3852             : {
    3853          13 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3854          13 :     pimpl_->resetHard();
    3855          13 :     auto repo = pimpl_->repository();
    3856          13 :     auto account = pimpl_->account_.lock();
    3857          13 :     if (!account || !repo)
    3858           0 :         return {};
    3859          13 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3860          13 :     auto cert = account->identity().second;
    3861          13 :     if (!cert || !cert->issuer)
    3862           0 :         return {};
    3863          13 :     auto adminUri = cert->issuer->getId().toString();
    3864          13 :     if (adminUri == uri) {
    3865           4 :         JAMI_WARNING("Admin tried to ban theirself");
    3866           1 :         return {};
    3867             :     }
    3868             : 
    3869          24 :     auto oldFile = repoPath / type / (uri + (type != "invited" ? ".crt" : ""));
    3870          12 :     if (!std::filesystem::is_regular_file(oldFile)) {
    3871           0 :         JAMI_WARNING("Didn't found file for {} with type {}", uri, type);
    3872           0 :         return {};
    3873             :     }
    3874             : 
    3875          12 :     auto relativeVotePath = fmt::format("votes/ban/{}/{}", type, uri);
    3876          12 :     auto voteDirectory = repoPath / relativeVotePath;
    3877          12 :     if (!dhtnet::fileutils::recursive_mkdir(voteDirectory, 0700)) {
    3878           0 :         JAMI_ERROR("Error when creating {}. Abort vote", voteDirectory);
    3879           0 :         return {};
    3880             :     }
    3881          12 :     auto votePath = fileutils::getFullPath(voteDirectory, adminUri);
    3882          12 :     std::ofstream voteFile(votePath, std::ios::trunc | std::ios::binary);
    3883          12 :     if (!voteFile.is_open()) {
    3884           0 :         JAMI_ERROR("Unable to write data to {}", votePath);
    3885           0 :         return {};
    3886             :     }
    3887          12 :     voteFile.close();
    3888             : 
    3889          12 :     auto toAdd = fmt::format("{}/{}", relativeVotePath, adminUri);
    3890          12 :     if (!pimpl_->add(toAdd))
    3891           0 :         return {};
    3892             : 
    3893          12 :     Json::Value json;
    3894          12 :     json["uri"] = uri;
    3895          12 :     json["type"] = "vote";
    3896          24 :     return pimpl_->commitMessage(json::toString(json));
    3897          13 : }
    3898             : 
    3899             : std::string
    3900           1 : ConversationRepository::voteUnban(const std::string& uri, const std::string_view type)
    3901             : {
    3902           1 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3903           1 :     pimpl_->resetHard();
    3904           1 :     auto repo = pimpl_->repository();
    3905           1 :     auto account = pimpl_->account_.lock();
    3906           1 :     if (!account || !repo)
    3907           0 :         return {};
    3908           1 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3909           1 :     auto cert = account->identity().second;
    3910           1 :     if (!cert || !cert->issuer)
    3911           0 :         return {};
    3912           1 :     auto adminUri = cert->issuer->getId().toString();
    3913             : 
    3914           1 :     auto relativeVotePath = fmt::format("votes/unban/{}/{}", type, uri);
    3915           1 :     auto voteDirectory = repoPath / relativeVotePath;
    3916           1 :     if (!dhtnet::fileutils::recursive_mkdir(voteDirectory, 0700)) {
    3917           0 :         JAMI_ERROR("Error when creating {}. Abort vote", voteDirectory);
    3918           0 :         return {};
    3919             :     }
    3920           1 :     auto votePath = voteDirectory / adminUri;
    3921           1 :     std::ofstream voteFile(votePath, std::ios::trunc | std::ios::binary);
    3922           1 :     if (!voteFile.is_open()) {
    3923           0 :         JAMI_ERROR("Unable to write data to {}", votePath);
    3924           0 :         return {};
    3925             :     }
    3926           1 :     voteFile.close();
    3927             : 
    3928           2 :     auto toAdd = fileutils::getFullPath(relativeVotePath, adminUri).string();
    3929           1 :     if (!pimpl_->add(toAdd.c_str()))
    3930           0 :         return {};
    3931             : 
    3932           1 :     Json::Value json;
    3933           1 :     json["uri"] = uri;
    3934           1 :     json["type"] = "vote";
    3935           2 :     return pimpl_->commitMessage(json::toString(json));
    3936           1 : }
    3937             : 
    3938             : bool
    3939          12 : ConversationRepository::Impl::resolveBan(const std::string_view type, const std::string& uri)
    3940             : {
    3941          12 :     auto repo = repository();
    3942          12 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3943          12 :     auto bannedPath = repoPath / "banned";
    3944          12 :     auto devicesPath = repoPath / "devices";
    3945             :     // Move from device or members file into banned
    3946          12 :     auto crtStr = uri + (type != "invited" ? ".crt" : "");
    3947          24 :     auto originFilePath = repoPath / type / crtStr;
    3948             : 
    3949          12 :     auto destPath = bannedPath / type;
    3950          12 :     auto destFilePath = destPath / crtStr;
    3951          12 :     if (!dhtnet::fileutils::recursive_mkdir(destPath, 0700)) {
    3952           0 :         JAMI_ERROR("An error occurred while creating the {} directory. Abort resolving vote.", destPath);
    3953           0 :         return false;
    3954             :     }
    3955             : 
    3956          12 :     std::error_code ec;
    3957          12 :     std::filesystem::rename(originFilePath, destFilePath, ec);
    3958          12 :     if (ec) {
    3959           0 :         JAMI_ERROR("An error occurred while moving the {} origin file path to the {} destination "
    3960             :                    "file path. Abort resolving vote.",
    3961             :                    originFilePath,
    3962             :                    destFilePath);
    3963           0 :         return false;
    3964             :     }
    3965             : 
    3966             :     // If members, remove related devices and mark as banned
    3967          12 :     if (type != "devices") {
    3968          12 :         std::error_code ec;
    3969          64 :         for (const auto& certificate : std::filesystem::directory_iterator(devicesPath, ec)) {
    3970          28 :             auto certPath = certificate.path();
    3971             :             try {
    3972          56 :                 crypto::Certificate cert(fileutils::loadFile(certPath));
    3973          28 :                 if (auto issuer = cert.issuer)
    3974           0 :                     if (issuer->getPublicKey().getId().to_view() == uri)
    3975          28 :                         dhtnet::fileutils::remove(certPath, true);
    3976          28 :             } catch (...) {
    3977           0 :                 continue;
    3978           0 :             }
    3979          40 :         }
    3980          12 :         std::lock_guard lk(membersMtx_);
    3981          12 :         auto updated = false;
    3982             : 
    3983          29 :         for (auto& member : members_) {
    3984          29 :             if (member.uri == uri) {
    3985          12 :                 updated = true;
    3986          12 :                 member.role = MemberRole::BANNED;
    3987          12 :                 break;
    3988             :             }
    3989             :         }
    3990          12 :         if (!updated)
    3991           0 :             members_.emplace_back(ConversationMember {uri, MemberRole::BANNED});
    3992          12 :         saveMembers();
    3993          12 :     }
    3994          12 :     return true;
    3995          12 : }
    3996             : 
    3997             : bool
    3998           1 : ConversationRepository::Impl::resolveUnban(const std::string_view type, const std::string& uri)
    3999             : {
    4000           1 :     auto repo = repository();
    4001           1 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    4002           1 :     auto bannedPath = repoPath / "banned";
    4003           1 :     auto crtStr = uri + (type != "invited" ? ".crt" : "");
    4004           2 :     auto originFilePath = bannedPath / type / crtStr;
    4005           1 :     auto destPath = repoPath / type;
    4006           1 :     auto destFilePath = destPath / crtStr;
    4007           1 :     if (!dhtnet::fileutils::recursive_mkdir(destPath, 0700)) {
    4008           0 :         JAMI_ERROR("An error occurred while creating the {} destination path. Abort resolving vote.", destPath);
    4009           0 :         return false;
    4010             :     }
    4011           1 :     std::error_code ec;
    4012           1 :     std::filesystem::rename(originFilePath, destFilePath, ec);
    4013           1 :     if (ec) {
    4014           0 :         JAMI_ERROR("Error when moving {} to {}. Abort resolving vote.", originFilePath, destFilePath);
    4015           0 :         return false;
    4016             :     }
    4017             : 
    4018           1 :     std::lock_guard lk(membersMtx_);
    4019           1 :     auto updated = false;
    4020             : 
    4021           1 :     auto role = MemberRole::MEMBER;
    4022           1 :     if (type == "invited")
    4023           0 :         role = MemberRole::INVITED;
    4024           1 :     else if (type == "admins")
    4025           0 :         role = MemberRole::ADMIN;
    4026             : 
    4027           2 :     for (auto& member : members_) {
    4028           2 :         if (member.uri == uri) {
    4029           1 :             updated = true;
    4030           1 :             member.role = role;
    4031           1 :             break;
    4032             :         }
    4033             :     }
    4034           1 :     if (!updated)
    4035           0 :         members_.emplace_back(ConversationMember {uri, role});
    4036           1 :     saveMembers();
    4037           1 :     return true;
    4038           1 : }
    4039             : 
    4040             : std::string
    4041          13 : ConversationRepository::resolveVote(const std::string& uri, const std::string_view type, const std::string& voteType)
    4042             : {
    4043          13 :     std::lock_guard lkOp(pimpl_->opMtx_);
    4044          13 :     pimpl_->resetHard();
    4045             :     // Count ratio admin/votes
    4046          13 :     auto nbAdmins = 0, nbVotes = 0;
    4047             :     // For each admin, check if voted
    4048          13 :     auto repo = pimpl_->repository();
    4049          13 :     if (!repo)
    4050           0 :         return {};
    4051          13 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    4052          13 :     auto adminsPath = repoPath / MemberPath::ADMINS;
    4053          26 :     auto voteDirectory = repoPath / "votes" / voteType / type / uri;
    4054          26 :     for (const auto& certificate : dhtnet::fileutils::readDirectory(adminsPath)) {
    4055          13 :         if (certificate.find(".crt") == std::string::npos) {
    4056           0 :             JAMI_WARNING("Incorrect file found: {}/{}", adminsPath, certificate);
    4057           0 :             continue;
    4058           0 :         }
    4059          26 :         auto adminUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
    4060          13 :         nbAdmins += 1;
    4061          13 :         if (std::filesystem::is_regular_file(fileutils::getFullPath(voteDirectory, adminUri)))
    4062          13 :             nbVotes += 1;
    4063          26 :     }
    4064             : 
    4065          13 :     if (nbAdmins > 0 && (static_cast<double>(nbVotes) / static_cast<double>(nbAdmins)) > .5) {
    4066          52 :         JAMI_WARNING("More than half of the admins voted to ban {}, applying the ban.", uri);
    4067             : 
    4068             :         // Remove vote directory
    4069          13 :         dhtnet::fileutils::removeAll(voteDirectory, true);
    4070             : 
    4071          13 :         if (voteType == "ban") {
    4072          12 :             if (!pimpl_->resolveBan(type, uri))
    4073           0 :                 return {};
    4074           1 :         } else if (voteType == "unban") {
    4075           1 :             if (!pimpl_->resolveUnban(type, uri))
    4076           0 :                 return {};
    4077             :         }
    4078             : 
    4079             :         // Commit
    4080          13 :         if (!git_add_all(repo.get()))
    4081           0 :             return {};
    4082             : 
    4083          13 :         Json::Value json;
    4084          13 :         json["action"] = voteType;
    4085          13 :         json["uri"] = uri;
    4086          13 :         json["type"] = "member";
    4087          26 :         return pimpl_->commitMessage(json::toString(json));
    4088          13 :     }
    4089             : 
    4090             :     // If vote nok
    4091           0 :     return {};
    4092          13 : }
    4093             : 
    4094             : std::pair<std::vector<ConversationCommit>, bool>
    4095        1919 : ConversationRepository::validFetch(const std::string& remoteDevice) const
    4096             : {
    4097        3838 :     auto newCommit = remoteHead(remoteDevice);
    4098        1919 :     if (not pimpl_ or newCommit.empty())
    4099           0 :         return {{}, false};
    4100        1919 :     auto commitsToValidate = pimpl_->behind(newCommit);
    4101        1919 :     std::reverse(std::begin(commitsToValidate), std::end(commitsToValidate));
    4102        1919 :     auto isValid = pimpl_->validCommits(commitsToValidate);
    4103        1919 :     if (isValid)
    4104        1902 :         return {commitsToValidate, false};
    4105          17 :     return {{}, true};
    4106        1919 : }
    4107             : 
    4108             : std::pair<std::vector<ConversationCommit>, bool>
    4109         190 : ConversationRepository::validClone() const
    4110             : {
    4111         190 :     auto commits = log({});
    4112         190 :     if (!pimpl_->validCommits(commits))
    4113           4 :         return {{}, false};
    4114         186 :     return {std::move(commits), true};
    4115         190 : }
    4116             : 
    4117             : bool
    4118           2 : ConversationRepository::isValidUserAtCommit(const std::string& userDevice,
    4119             :                                             const std::string& commitId,
    4120             :                                             const git_buf& sig,
    4121             :                                             const git_buf& sig_data) const
    4122             : {
    4123           2 :     return pimpl_->isValidUserAtCommit(userDevice, commitId, sig, sig_data);
    4124             : }
    4125             : 
    4126             : bool
    4127           5 : ConversationRepository::validCommits(const std::vector<ConversationCommit>& commitsToValidate) const
    4128             : {
    4129           5 :     return pimpl_->validCommits(commitsToValidate);
    4130             : }
    4131             : 
    4132             : void
    4133        1010 : ConversationRepository::removeBranchWith(const std::string& remoteDevice)
    4134             : {
    4135        1010 :     git_remote* remote_ptr = nullptr;
    4136        1010 :     auto repo = pimpl_->repository();
    4137        1010 :     if (!repo || git_remote_lookup(&remote_ptr, repo.get(), remoteDevice.c_str()) < 0) {
    4138           4 :         JAMI_WARNING("No remote found with id: {}", remoteDevice);
    4139           1 :         return;
    4140             :     }
    4141        1009 :     GitRemote remote {remote_ptr};
    4142             : 
    4143        1009 :     git_remote_prune(remote.get(), nullptr);
    4144        1010 : }
    4145             : 
    4146             : std::vector<std::string>
    4147          30 : ConversationRepository::getInitialMembers() const
    4148             : {
    4149          30 :     return pimpl_->getInitialMembers();
    4150             : }
    4151             : 
    4152             : std::vector<ConversationMember>
    4153        1352 : ConversationRepository::members() const
    4154             : {
    4155        1352 :     return pimpl_->members();
    4156             : }
    4157             : 
    4158             : std::set<std::string>
    4159        2981 : ConversationRepository::memberUris(std::string_view filter, const std::set<MemberRole>& filteredRoles) const
    4160             : {
    4161        2981 :     return pimpl_->memberUris(filter, filteredRoles);
    4162             : }
    4163             : 
    4164             : std::map<std::string, std::vector<DeviceId>>
    4165         513 : ConversationRepository::devices(bool ignoreExpired) const
    4166             : {
    4167         513 :     return pimpl_->devices(ignoreExpired);
    4168             : }
    4169             : 
    4170             : void
    4171         773 : ConversationRepository::refreshMembers() const
    4172             : {
    4173             :     try {
    4174         773 :         pimpl_->initMembers();
    4175           0 :     } catch (...) {
    4176           0 :     }
    4177         773 : }
    4178             : 
    4179             : void
    4180         190 : ConversationRepository::pinCertificates(bool blocking)
    4181             : {
    4182         190 :     auto acc = pimpl_->account_.lock();
    4183         190 :     auto repo = pimpl_->repository();
    4184         190 :     if (!repo or !acc)
    4185           0 :         return;
    4186             : 
    4187         190 :     std::string repoPath = git_repository_workdir(repo.get());
    4188         190 :     std::vector<std::string> paths = {repoPath + MemberPath::ADMINS.string(),
    4189         380 :                                       repoPath + MemberPath::MEMBERS.string(),
    4190        1330 :                                       repoPath + MemberPath::DEVICES.string()};
    4191             : 
    4192         760 :     for (const auto& path : paths) {
    4193         570 :         if (blocking) {
    4194         570 :             std::promise<bool> p;
    4195         570 :             std::future<bool> f = p.get_future();
    4196        1140 :             acc->certStore().pinCertificatePath(path, [&](auto /* certs */) { p.set_value(true); });
    4197         570 :             f.wait();
    4198         570 :         } else {
    4199           0 :             acc->certStore().pinCertificatePath(path, {});
    4200             :         }
    4201             :     }
    4202         190 : }
    4203             : 
    4204             : std::string
    4205       13458 : ConversationRepository::uriFromDevice(const std::string& deviceId) const
    4206             : {
    4207       13458 :     return pimpl_->uriFromDevice(deviceId);
    4208             : }
    4209             : 
    4210             : std::string
    4211           9 : ConversationRepository::updateInfos(const std::map<std::string, std::string>& profile)
    4212             : {
    4213           9 :     std::lock_guard lkOp(pimpl_->opMtx_);
    4214           9 :     pimpl_->resetHard();
    4215           9 :     auto valid = false;
    4216             :     {
    4217           9 :         std::lock_guard lk(pimpl_->membersMtx_);
    4218          10 :         for (const auto& member : pimpl_->members_) {
    4219          10 :             if (member.uri == pimpl_->userId_) {
    4220           9 :                 valid = member.role <= pimpl_->updateProfilePermLvl_;
    4221           9 :                 break;
    4222             :             }
    4223             :         }
    4224           9 :     }
    4225           9 :     if (!valid) {
    4226           4 :         JAMI_ERROR("Insufficient permission to update information.");
    4227           1 :         emitSignal<libjami::ConversationSignal::OnConversationError>(pimpl_->accountId_,
    4228           1 :                                                                      pimpl_->id_,
    4229             :                                                                      EUNAUTHORIZED,
    4230             :                                                                      "Insufficient permission to update information.");
    4231           1 :         return {};
    4232             :     }
    4233             : 
    4234           8 :     auto infosMap = infos();
    4235          19 :     for (const auto& [k, v] : profile) {
    4236          11 :         infosMap[k] = v;
    4237             :     }
    4238           8 :     auto repo = pimpl_->repository();
    4239           8 :     if (!repo)
    4240           0 :         return {};
    4241           8 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    4242           8 :     auto profilePath = repoPath / "profile.vcf";
    4243           8 :     std::ofstream file(profilePath, std::ios::trunc | std::ios::binary);
    4244           8 :     if (!file.is_open()) {
    4245           0 :         JAMI_ERROR("Unable to write data to {}", profilePath);
    4246           0 :         return {};
    4247             :     }
    4248             : 
    4249          32 :     auto addKey = [&](auto property, auto key) {
    4250          32 :         auto it = infosMap.find(std::string(key));
    4251          32 :         if (it != infosMap.end()) {
    4252          11 :             file << property;
    4253          11 :             file << ":";
    4254          11 :             file << it->second;
    4255          11 :             file << vCard::Delimiter::END_LINE_TOKEN;
    4256             :         }
    4257          32 :     };
    4258             : 
    4259           8 :     file << vCard::Delimiter::BEGIN_TOKEN;
    4260           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4261           8 :     file << vCard::Property::VCARD_VERSION;
    4262           8 :     file << ":2.1";
    4263           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4264           8 :     addKey(vCard::Property::FORMATTED_NAME, vCard::Value::TITLE);
    4265           8 :     addKey(vCard::Property::DESCRIPTION, vCard::Value::DESCRIPTION);
    4266           8 :     file << vCard::Property::PHOTO;
    4267           8 :     file << vCard::Delimiter::SEPARATOR_TOKEN;
    4268           8 :     file << vCard::Property::BASE64;
    4269           8 :     auto avatarIt = infosMap.find(std::string(vCard::Value::AVATAR));
    4270           8 :     if (avatarIt != infosMap.end()) {
    4271             :         // TODO: type=png? store another way?
    4272           0 :         file << ":";
    4273           0 :         file << avatarIt->second;
    4274             :     }
    4275           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4276           8 :     addKey(vCard::Property::RDV_ACCOUNT, vCard::Value::RDV_ACCOUNT);
    4277           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4278           8 :     addKey(vCard::Property::RDV_DEVICE, vCard::Value::RDV_DEVICE);
    4279           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4280           8 :     file << vCard::Delimiter::END_TOKEN;
    4281           8 :     file.close();
    4282             : 
    4283           8 :     if (!pimpl_->add("profile.vcf"))
    4284           0 :         return {};
    4285           8 :     Json::Value json;
    4286           8 :     json["type"] = "application/update-profile";
    4287          16 :     return pimpl_->commitMessage(json::toString(json));
    4288           9 : }
    4289             : 
    4290             : std::map<std::string, std::string>
    4291         337 : ConversationRepository::infos() const
    4292             : {
    4293         337 :     if (auto repo = pimpl_->repository()) {
    4294             :         try {
    4295         337 :             std::filesystem::path repoPath = git_repository_workdir(repo.get());
    4296         337 :             auto profilePath = repoPath / "profile.vcf";
    4297         337 :             std::map<std::string, std::string> result;
    4298         337 :             std::error_code ec;
    4299         337 :             if (std::filesystem::is_regular_file(profilePath, ec)) {
    4300          25 :                 auto content = fileutils::loadFile(profilePath);
    4301          50 :                 result = ConversationRepository::infosFromVCard(
    4302          75 :                     vCard::utils::toMap(std::string_view {(const char*) content.data(), content.size()}));
    4303          25 :             }
    4304         337 :             result["mode"] = std::to_string(static_cast<int>(mode()));
    4305         337 :             return result;
    4306         337 :         } catch (...) {
    4307           0 :         }
    4308         337 :     }
    4309           0 :     return {};
    4310             : }
    4311             : 
    4312             : std::map<std::string, std::string>
    4313         101 : ConversationRepository::infosFromVCard(vCard::utils::VCardData&& details)
    4314             : {
    4315         101 :     std::map<std::string, std::string> result;
    4316         217 :     for (auto&& [k, v] : details) {
    4317         116 :         if (k == vCard::Property::FORMATTED_NAME) {
    4318          15 :             result["title"] = std::move(v);
    4319         101 :         } else if (k == vCard::Property::DESCRIPTION) {
    4320           1 :             result["description"] = std::move(v);
    4321         100 :         } else if (k.find(vCard::Property::PHOTO) == 0) {
    4322           0 :             result["avatar"] = std::move(v);
    4323         100 :         } else if (k.find(vCard::Property::RDV_ACCOUNT) == 0) {
    4324          11 :             result["rdvAccount"] = std::move(v);
    4325          89 :         } else if (k.find(vCard::Property::RDV_DEVICE) == 0) {
    4326          11 :             result["rdvDevice"] = std::move(v);
    4327             :         }
    4328             :     }
    4329         101 :     return result;
    4330           0 : }
    4331             : 
    4332             : std::string
    4333        1919 : ConversationRepository::getHead() const
    4334             : {
    4335        1919 :     if (auto repo = pimpl_->repository()) {
    4336             :         git_oid commit_id;
    4337        1919 :         if (git_reference_name_to_id(&commit_id, repo.get(), "HEAD") < 0) {
    4338           0 :             JAMI_ERROR("Unable to get reference for HEAD");
    4339        1919 :             return {};
    4340             :         }
    4341        1919 :         if (auto commit_str = git_oid_tostr_s(&commit_id))
    4342        1918 :             return commit_str;
    4343        1919 :     }
    4344           0 :     return {};
    4345             : }
    4346             : 
    4347             : std::optional<std::map<std::string, std::string>>
    4348        6195 : ConversationRepository::convCommitToMap(const ConversationCommit& commit) const
    4349             : {
    4350        6195 :     return pimpl_->convCommitToMap(commit);
    4351             : }
    4352             : 
    4353             : std::vector<std::map<std::string, std::string>>
    4354        1392 : ConversationRepository::convCommitsToMap(const std::vector<ConversationCommit>& commits) const
    4355             : {
    4356        1392 :     std::vector<std::map<std::string, std::string>> result = {};
    4357        1392 :     result.reserve(commits.size());
    4358        3618 :     for (const auto& commit : commits) {
    4359        2227 :         if (auto message = pimpl_->convCommitToMap(commit))
    4360        2227 :             result.emplace_back(*message);
    4361             :     }
    4362        1391 :     return result;
    4363           0 : }
    4364             : 
    4365             : } // namespace jami

Generated by: LCOV version 1.14