LCOV - code coverage report
Current view: top level - src/jamidht - conversationrepository.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 72.7 % 2991 2173
Test Date: 2026-11-09 10:41:14 Functions: 99.3 % 144 143

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

Generated by: LCOV version 2.0-1