LCOV - code coverage report
Current view: top level - foo/src/jamidht - conversationrepository.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1902 2587 73.5 %
Date: 2026-02-28 10:41:24 Functions: 225 637 35.3 %

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

Generated by: LCOV version 1.14