LCOV - code coverage report
Current view: top level - foo/src/jamidht - conversationrepository.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1868 2550 73.3 %
Date: 2025-12-18 10:07:43 Functions: 224 626 35.8 %

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

Generated by: LCOV version 1.14