LCOV - code coverage report
Current view: top level - foo/src/jamidht - conversationrepository.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1873 2560 73.2 %
Date: 2025-10-16 08:11:43 Functions: 216 616 35.1 %

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

Generated by: LCOV version 1.14