LCOV - code coverage report
Current view: top level - src/plugin - pluginsutils.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 79.1 % 153 121
Test Date: 2026-07-06 08:25:38 Functions: 67.7 % 31 21

            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 "pluginsutils.h"
      19              : #include "logger.h"
      20              : #include "fileutils.h"
      21              : #include "archiver.h"
      22              : #include "string_utils.h"
      23              : 
      24              : #include <msgpack.hpp>
      25              : 
      26              : #include <fstream>
      27              : #include <regex>
      28              : 
      29              : #if defined(__APPLE__)
      30              : #if (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
      31              : #define ABI "iphone"
      32              : #else
      33              : #if defined(__x86_64__)
      34              : #define ABI "x86_64-apple-Darwin"
      35              : #else
      36              : #define ABI "arm64-apple-Darwin"
      37              : #endif
      38              : #endif
      39              : #elif defined(__arm__)
      40              : #if defined(__ARM_ARCH_7A__)
      41              : #define ABI "armeabi-v7a"
      42              : #else
      43              : #define ABI "armeabi"
      44              : #endif
      45              : #elif defined(__i386__)
      46              : #if __ANDROID__
      47              : #define ABI "x86"
      48              : #else
      49              : #define ABI "x86-linux-gnu"
      50              : #endif
      51              : #elif defined(__x86_64__)
      52              : #if __ANDROID__
      53              : #define ABI "x86_64"
      54              : #else
      55              : #define ABI "x86_64-linux-gnu"
      56              : #endif
      57              : #elif defined(__aarch64__)
      58              : #define ABI "arm64-v8a"
      59              : #elif defined(WIN32)
      60              : #define ABI "x64-windows"
      61              : #else
      62              : #define ABI "unknown"
      63              : #endif
      64              : 
      65              : namespace jami {
      66              : namespace PluginUtils {
      67              : 
      68              : // DATA_REGEX is used to during the plugin jpl uncompressing
      69              : const std::regex DATA_REGEX("^data" DIR_SEPARATOR_STR_ESC ".+");
      70              : // SO_REGEX is used to find libraries during the plugin jpl uncompressing
      71              : // lib/ABI/libplugin.SO
      72              : static constexpr char SO_PATTERN[] = DIR_SEPARATOR_STR_ESC "(.*)" DIR_SEPARATOR_STR_ESC
      73              :                                                            "([a-zA-Z0-9_]+.(dylib|so|dll|lib).*)";
      74              : const std::regex SO_REGEX(SO_PATTERN);
      75              : 
      76              : std::filesystem::path
      77           25 : manifestPath(const std::filesystem::path& rootPath)
      78              : {
      79           25 :     return rootPath / "manifest.json";
      80              : }
      81              : 
      82              : std::map<std::string, std::string>
      83            0 : getPlatformInfo()
      84              : {
      85            0 :     return {{"os", ABI}};
      86            0 : }
      87              : 
      88              : std::filesystem::path
      89           10 : getRootPathFromSoPath(const std::filesystem::path& soPath)
      90              : {
      91           10 :     return soPath.parent_path();
      92              : }
      93              : 
      94              : std::filesystem::path
      95            8 : dataPath(const std::filesystem::path& pluginSoPath)
      96              : {
      97           16 :     return getRootPathFromSoPath(pluginSoPath) / "data";
      98              : }
      99              : 
     100              : std::map<std::string, std::string>
     101           52 : checkManifestJsonContentValidity(const Json::Value& root)
     102              : {
     103           52 :     std::string name = root.get("name", "").asString();
     104           52 :     std::string id = root.get("id", name).asString();
     105           52 :     std::string description = root.get("description", "").asString();
     106           52 :     std::string version = root.get("version", "").asString();
     107           52 :     std::string iconPath = root.get("iconPath", "icon.png").asString();
     108           52 :     std::string background = root.get("backgroundPath", "background.jpg").asString();
     109           52 :     if (!name.empty() || !version.empty()) {
     110              :         return {
     111              :             {"id", id},
     112              :             {"name", name},
     113              :             {"description", description},
     114              :             {"version", version},
     115              :             {"iconPath", iconPath},
     116              :             {"backgroundPath", background},
     117          468 :         };
     118              :     } else {
     119            0 :         throw std::runtime_error("plugin manifest file: bad format");
     120              :     }
     121          104 : }
     122              : 
     123              : std::map<std::string, std::string>
     124            0 : checkManifestValidity(std::istream& stream)
     125              : {
     126            0 :     Json::Value root;
     127            0 :     Json::CharReaderBuilder rbuilder;
     128            0 :     rbuilder["collectComments"] = false;
     129            0 :     std::string errs;
     130              : 
     131            0 :     if (Json::parseFromStream(rbuilder, stream, &root, &errs)) {
     132            0 :         return checkManifestJsonContentValidity(root);
     133              :     } else {
     134            0 :         throw std::runtime_error("failed to parse the plugin manifest file");
     135              :     }
     136            0 : }
     137              : 
     138              : std::map<std::string, std::string>
     139           52 : checkManifestValidity(const std::vector<uint8_t>& vec)
     140              : {
     141           52 :     Json::Value root;
     142           52 :     std::unique_ptr<Json::CharReader> json_Reader(Json::CharReaderBuilder {}.newCharReader());
     143           52 :     std::string errs;
     144              : 
     145          104 :     bool ok = json_Reader->parse(reinterpret_cast<const char*>(vec.data()),
     146           52 :                                  reinterpret_cast<const char*>(vec.data() + vec.size()),
     147              :                                  &root,
     148              :                                  &errs);
     149              : 
     150           52 :     if (ok) {
     151          104 :         return checkManifestJsonContentValidity(root);
     152              :     } else {
     153            0 :         throw std::runtime_error("failed to parse the plugin manifest file");
     154              :     }
     155           52 : }
     156              : 
     157              : std::map<std::string, std::string>
     158           25 : parseManifestFile(const std::filesystem::path& manifestFilePath, const std::string& rootPath)
     159              : {
     160           25 :     std::lock_guard guard(dhtnet::fileutils::getFileLock(manifestFilePath));
     161           25 :     std::ifstream file(manifestFilePath);
     162           25 :     if (file) {
     163              :         try {
     164           20 :             const auto& traduction = parseManifestTranslation(rootPath, file);
     165           20 :             return checkManifestValidity(std::vector<uint8_t>(traduction.begin(), traduction.end()));
     166           20 :         } catch (const std::exception& e) {
     167            0 :             JAMI_ERROR("{}", e.what());
     168            0 :         }
     169              :     }
     170            5 :     return {};
     171           25 : }
     172              : 
     173              : std::string
     174           20 : parseManifestTranslation(const std::string& rootPath, std::ifstream& manifestFile)
     175              : {
     176           20 :     if (manifestFile) {
     177           20 :         std::stringstream buffer;
     178           20 :         buffer << manifestFile.rdbuf();
     179           20 :         std::string manifest = buffer.str();
     180           20 :         const auto& translation = getLocales(rootPath, getLanguage());
     181           20 :         std::regex pattern(R"(\{\{([^}]+)\}\})");
     182           20 :         std::smatch matches;
     183              :         // replace the pattern to the correct translation
     184           60 :         while (std::regex_search(manifest, matches, pattern)) {
     185           20 :             if (matches.size() == 2) {
     186           20 :                 auto it = translation.find(matches[1].str());
     187           20 :                 if (it == translation.end()) {
     188            0 :                     manifest = std::regex_replace(manifest, pattern, "");
     189            0 :                     continue;
     190              :                 }
     191           20 :                 manifest = std::regex_replace(manifest, pattern, it->second, std::regex_constants::format_first_only);
     192              :             }
     193              :         }
     194           20 :         return manifest;
     195           20 :     }
     196            0 :     return {};
     197              : }
     198              : 
     199              : bool
     200            8 : checkPluginValidity(const std::filesystem::path& rootPath)
     201              : {
     202            8 :     return !parseManifestFile(manifestPath(rootPath), rootPath.string()).empty();
     203              : }
     204              : 
     205              : std::map<std::string, std::string>
     206           32 : readPluginManifestFromArchive(const std::string& jplPath)
     207              : {
     208              :     try {
     209           64 :         return checkManifestValidity(archiver::readFileFromArchive(jplPath, "manifest.json"));
     210            0 :     } catch (const std::exception& e) {
     211            0 :         JAMI_ERROR("{}", e.what());
     212            0 :     }
     213            0 :     return {};
     214              : }
     215              : 
     216              : std::unique_ptr<dht::crypto::Certificate>
     217           10 : readPluginCertificate(const std::string& rootPath, const std::string& pluginId)
     218              : {
     219           10 :     std::string certPath = rootPath + DIR_SEPARATOR_CH + pluginId + ".crt";
     220              :     try {
     221           10 :         auto cert = fileutils::loadFile(certPath);
     222           10 :         return std::make_unique<dht::crypto::Certificate>(cert);
     223           10 :     } catch (const std::exception& e) {
     224            0 :         JAMI_ERROR("{}", e.what());
     225            0 :     }
     226            0 :     return {};
     227           10 : }
     228              : 
     229              : std::unique_ptr<dht::crypto::Certificate>
     230           14 : readPluginCertificateFromArchive(const std::string& jplPath)
     231              : {
     232              :     try {
     233           14 :         auto manifest = readPluginManifestFromArchive(jplPath);
     234           14 :         const std::string& name = manifest["id"];
     235              : 
     236           14 :         if (name.empty()) {
     237            0 :             return {};
     238              :         }
     239           14 :         return std::make_unique<dht::crypto::Certificate>(archiver::readFileFromArchive(jplPath, name + ".crt"));
     240           14 :     } catch (const std::exception& e) {
     241            0 :         JAMI_ERROR("{}", e.what());
     242            0 :         return {};
     243            0 :     }
     244              : }
     245              : 
     246              : std::map<std::string, std::vector<uint8_t>>
     247           20 : readPluginSignatureFromArchive(const std::string& jplPath)
     248              : {
     249              :     try {
     250           21 :         std::vector<uint8_t> vec = archiver::readFileFromArchive(jplPath, "signatures");
     251           19 :         msgpack::object_handle oh = msgpack::unpack(reinterpret_cast<const char*>(vec.data()),
     252           38 :                                                     vec.size() * sizeof(uint8_t));
     253           19 :         msgpack::object obj = oh.get();
     254           19 :         return obj.as<std::map<std::string, std::vector<uint8_t>>>();
     255           20 :     } catch (const std::exception& e) {
     256            4 :         JAMI_ERROR("{}", e.what());
     257            1 :         return {};
     258            1 :     }
     259              : }
     260              : 
     261              : std::vector<uint8_t>
     262           10 : readSignatureFileFromArchive(const std::string& jplPath)
     263              : {
     264           20 :     return archiver::readFileFromArchive(jplPath, "signatures.sig");
     265              : }
     266              : 
     267              : std::pair<bool, std::string_view>
     268          105 : uncompressJplFunction(std::string_view relativeFileName)
     269              : {
     270          105 :     std::svmatch match;
     271              :     // manifest.json and files under data/ folder remains in the same structure
     272              :     // but libraries files are extracted from the folder that matches the running ABI to
     273              :     // the main installation path.
     274          105 :     if (std::regex_search(relativeFileName, match, SO_REGEX)) {
     275            7 :         if (std::svsub_match_view(match[1]) != ABI) {
     276            0 :             return std::make_pair(false, std::string_view {});
     277              :         } else {
     278            7 :             return std::make_pair(true, std::svsub_match_view(match[2]));
     279              :         }
     280              :     }
     281           98 :     return std::make_pair(true, relativeFileName);
     282          105 : }
     283              : 
     284              : std::string
     285          133 : getLanguage()
     286              : {
     287          133 :     std::string lang;
     288          133 :     if (auto* envLang = std::getenv("JAMI_LANG"))
     289          132 :         lang = envLang;
     290              :     else
     291            4 :         JAMI_LOG("Error getting JAMI_LANG env, attempting to get system language");
     292              :     // If language preference is empty, try to get from the system.
     293          133 :     if (lang.empty()) {
     294              : #ifdef WIN32
     295              :         WCHAR localeBuffer[LOCALE_NAME_MAX_LENGTH];
     296              :         if (GetUserDefaultLocaleName(localeBuffer, LOCALE_NAME_MAX_LENGTH) != 0) {
     297              :             char utf8Buffer[LOCALE_NAME_MAX_LENGTH] {};
     298              :             WideCharToMultiByte(CP_UTF8,
     299              :                                 0,
     300              :                                 localeBuffer,
     301              :                                 LOCALE_NAME_MAX_LENGTH,
     302              :                                 utf8Buffer,
     303              :                                 LOCALE_NAME_MAX_LENGTH,
     304              :                                 nullptr,
     305              :                                 nullptr);
     306              : 
     307              :             lang.append(utf8Buffer);
     308              :             string_replace(lang, "-", "_");
     309              :         }
     310              :         // Even though we default to the system variable in Windows, technically this
     311              :         // part of the code should not be reached because the client-qt must define that
     312              :         // variable and is unable to run the client and the daemon in diferent processes in Windows.
     313              : #else
     314              :         // The same way described in the comment just above, Android should not reach this
     315              :         // part of the code given the client-android must define "JAMI_LANG" system variable.
     316              :         // And even if this part is reached, it should not work since std::locale is not
     317              :         // supported by the NDK.
     318              : 
     319              :         // LC_COLLATE is used to grab the locale for the case when the system user has set different
     320              :         // values for the preferred Language and Format.
     321            1 :         lang = setlocale(LC_COLLATE, "");
     322              :         // We set the environment to avoid checking from system everytime.
     323              :         // This is the case when running daemon and client in different processes
     324              :         // like with dbus.
     325            1 :         setenv("JAMI_LANG", lang.c_str(), 1);
     326              : #endif // WIN32
     327              :     }
     328          133 :     return lang;
     329            0 : }
     330              : 
     331              : std::map<std::string, std::string>
     332          133 : getLocales(const std::string& rootPath, const std::string& lang)
     333              : {
     334          133 :     auto pluginName = rootPath.substr(rootPath.find_last_of(DIR_SEPARATOR_CH) + 1);
     335          266 :     auto basePath = fmt::format("{}/data/locale/{}", rootPath, pluginName + "_");
     336              : 
     337          133 :     std::map<std::string, std::string> locales = {};
     338              : 
     339              :     // Get language translations
     340          133 :     if (!lang.empty()) {
     341          133 :         locales = processLocaleFile(basePath + lang + ".json");
     342              :     }
     343              : 
     344              :     // Get default english values if no translations were found
     345          133 :     if (locales.empty()) {
     346           41 :         locales = processLocaleFile(basePath + "en.json");
     347              :     }
     348              : 
     349          266 :     return locales;
     350          133 : }
     351              : 
     352              : std::map<std::string, std::string>
     353          174 : processLocaleFile(const std::string& preferenceLocaleFilePath)
     354              : {
     355          174 :     std::error_code ec;
     356          174 :     if (!std::filesystem::is_regular_file(preferenceLocaleFilePath, ec)) {
     357           41 :         return {};
     358              :     }
     359          133 :     std::ifstream file(preferenceLocaleFilePath);
     360          133 :     Json::Value root;
     361          133 :     Json::CharReaderBuilder rbuilder;
     362          399 :     rbuilder["collectComments"] = false;
     363          133 :     std::string errs;
     364          133 :     std::map<std::string, std::string> locales {};
     365          133 :     if (file) {
     366              :         // Read the file to a json format
     367          133 :         if (Json::parseFromStream(rbuilder, file, &root, &errs)) {
     368          133 :             auto keys = root.getMemberNames();
     369         1064 :             for (const auto& key : keys) {
     370          931 :                 locales[key] = root.get(key, "").asString();
     371              :             }
     372          133 :         }
     373              :     }
     374          133 :     return locales;
     375          133 : }
     376              : } // namespace PluginUtils
     377              : } // namespace jami
        

Generated by: LCOV version 2.0-1