LCOV - code coverage report
Current view: top level - src/jamidht - conversationrepository.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 73.5 % 2718 1997
Test Date: 2026-08-23 08:52:56 Functions: 99.2 % 120 119

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

Generated by: LCOV version 2.0-1