LCOV - code coverage report
Current view: top level - src/jamidht - conversationrepository.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 73.1 % 2791 2041
Test Date: 2026-09-13 09:08:58 Functions: 99.2 % 126 125

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

Generated by: LCOV version 2.0-1