LCOV - code coverage report
Current view: top level - src/jamidht - conversationrepository.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 71.9 % 2571 1848
Test Date: 2026-07-06 08:25:38 Functions: 34.4 % 648 223

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

Generated by: LCOV version 2.0-1