LCOV - code coverage report
Current view: top level - muse/framework/extensions/internal - extensionsloader.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 46.0 % 161 74
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2024 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : #include "extensionsloader.h"
      23              : 
      24              : #include "extensionstypes.h"
      25              : #include "global/io/dir.h"
      26              : #include "global/io/file.h"
      27              : #include "global/io/fileinfo.h"
      28              : #include "global/serialization/json.h"
      29              : #include "global/stringutils.h"
      30              : #include "global/runtime.h"
      31              : 
      32              : #include "log.h"
      33              : #include "types/ret.h"
      34              : 
      35              : using namespace muse;
      36              : using namespace muse::extensions;
      37              : 
      38              : const std::string MANIFEST("manifest.json");
      39              : const std::string DEV_EXTENSIONS("extensions/dev/");
      40              : 
      41              : namespace {
      42              : Val jsonValue(const JsonValue& value);
      43              : 
      44            2 : ValMap jsonMap(const JsonObject& object)
      45              : {
      46            2 :     ValMap result;
      47            8 :     for (const std::string& key : object.keys()) {
      48            6 :         result.emplace(key, jsonValue(object.value(key)));
      49            2 :     }
      50            2 :     return result;
      51            0 : }
      52              : 
      53            1 : ValList jsonList(const JsonArray& array)
      54              : {
      55            1 :     ValList result;
      56            1 :     result.reserve(array.size());
      57            3 :     for (size_t index = 0; index < array.size(); ++index) {
      58            2 :         result.push_back(jsonValue(array.at(index)));
      59              :     }
      60            1 :     return result;
      61            0 : }
      62              : 
      63            8 : Val jsonValue(const JsonValue& value)
      64              : {
      65            8 :     if (value.isBool()) {
      66            1 :         return Val(value.toBool());
      67              :     }
      68            7 :     if (value.isNumber()) {
      69            1 :         return Val(value.toDouble());
      70              :     }
      71            6 :     if (value.isString()) {
      72            4 :         return Val(value.toStdString());
      73              :     }
      74            2 :     if (value.isArray()) {
      75            1 :         return Val(jsonList(value.toArray()));
      76              :     }
      77            1 :     if (value.isObject()) {
      78            1 :         return Val(jsonMap(value.toObject()));
      79              :     }
      80            0 :     return {};
      81              : }
      82              : } // namespace
      83              : 
      84            0 : ManifestList ExtensionsLoader::loadManifestList(const io::path_t& defPath, const io::path_t& extPath) const
      85              : {
      86            0 :     TRACEFUNC;
      87              : 
      88            0 :     LOGD() << "try load extensions, def: " << defPath << ", user: " << extPath;
      89              : 
      90            0 :     ManifestList defaultManifests = manifestList(defPath);
      91            0 :     ManifestList externalManifests = manifestList(extPath);
      92              : 
      93            0 :     ManifestList retList;
      94            0 :     for (const Manifest& m : defaultManifests) {
      95            0 :         if (!m.isValid()) {
      96            0 :             continue;
      97              :         }
      98              : 
      99            0 :         if (!runtime::isDebug() && muse::strings::startsWith(m.uri.path(), DEV_EXTENSIONS)) {
     100            0 :             continue;
     101              :         }
     102              : 
     103            0 :         retList.push_back(m);
     104              :     }
     105              : 
     106            0 :     for (Manifest& m : externalManifests) {
     107            0 :         if (!m.isValid()) {
     108            0 :             continue;
     109              :         }
     110              : 
     111            0 :         if (!runtime::isDebug() && muse::strings::startsWith(m.uri.path(), DEV_EXTENSIONS)) {
     112            0 :             continue;
     113              :         }
     114              : 
     115            0 :         m.isRemovable = true;
     116            0 :         retList.push_back(m);
     117              :     }
     118              : 
     119            0 :     return retList;
     120            0 : }
     121              : 
     122            0 : ManifestList ExtensionsLoader::manifestList(const io::path_t& rootPath) const
     123              : {
     124            0 :     ManifestList manifests;
     125            0 :     io::paths_t paths = manifestPaths(rootPath);
     126            0 :     for (const io::path_t& path : paths) {
     127            0 :         LOGD() << "parsing manifest: " << path;
     128            0 :         RetVal<Manifest> rv = parseManifest(path);
     129            0 :         if (!rv.ret) {
     130            0 :             LOGE() << "failed parse manifest: " << path << ", err: " << rv.ret.toString();
     131            0 :             continue;
     132              :         }
     133            0 :         Manifest& manifest = rv.val;
     134            0 :         manifest.path = path;
     135            0 :         resolvePaths(manifest, io::FileInfo(path).dirPath());
     136            0 :         manifests.push_back(manifest);
     137            0 :     }
     138              : 
     139            0 :     return manifests;
     140            0 : }
     141              : 
     142            0 : io::paths_t ExtensionsLoader::manifestPaths(const io::path_t& rootPath) const
     143              : {
     144            0 :     RetVal<io::paths_t> paths = io::Dir::scanFiles(rootPath, { MANIFEST });
     145            0 :     if (!paths.ret) {
     146            0 :         LOGE() << "failed scan files, err: " << paths.ret.toString();
     147              :     }
     148            0 :     return paths.val;
     149            0 : }
     150              : 
     151            0 : RetVal<Manifest> ExtensionsLoader::parseManifest(const io::path_t& path) const
     152              : {
     153            0 :     ByteArray data;
     154            0 :     Ret ret = io::File::readFile(path, data);
     155            0 :     if (!ret) {
     156            0 :         return RetVal<Manifest>::make_ret(ret);
     157              :     }
     158              : 
     159            0 :     return parseManifest(data);
     160            0 : }
     161              : 
     162            1 : RetVal<Manifest> ExtensionsLoader::parseManifest(const ByteArray& data) const
     163              : {
     164            1 :     std::string jsonErr;
     165            1 :     JsonObject obj = JsonDocument::fromJson(data, &jsonErr).rootObject();
     166            1 :     if (!jsonErr.empty()) {
     167            0 :         return RetVal<Manifest>::make_ret(Ret::Code::BadData,
     168            0 :                                           "failed parse json, err: " + jsonErr
     169            0 :                                           );
     170              :     }
     171              : 
     172            4 :     auto check_required = [](const JsonObject& obj, const std::string& key) {
     173            4 :         if (!obj.contains(key)) {
     174              :             return muse::make_ret(Ret::Code::BadData,
     175            0 :                                   key + " is required but not specified"
     176            0 :                                   );
     177              :         }
     178            4 :         return muse::make_ok();
     179              :     };
     180              : 
     181            1 :     Manifest m;
     182              :     // required
     183            1 :     Ret ret = check_required(obj, "uri");
     184            1 :     if (!ret) {
     185            0 :         return RetVal<Manifest>::make_ret(ret);
     186              :     }
     187            3 :     m.uri = Uri(obj.value("uri").toStdString());
     188            1 :     if (m.uri.scheme() != EXTENSION_SCHEME) {
     189            1 :         LOGW() << "extension uri must have the scheme - " << EXTENSION_SCHEME << "://, uri: " << m.uri;
     190            2 :         m.uri.setScheme(std::string(EXTENSION_SCHEME));
     191              :     }
     192              : 
     193            1 :     ret = check_required(obj, "type");
     194            1 :     if (!ret) {
     195            0 :         return RetVal<Manifest>::make_ret(ret);
     196              :     }
     197            3 :     m.type = typeFromString(obj.value("type").toStdString());
     198              : 
     199            1 :     ret = check_required(obj, "title");
     200            1 :     if (!ret) {
     201            0 :         return RetVal<Manifest>::make_ret(ret);
     202              :     }
     203            3 :     m.title = obj.value("title").toString();
     204              : 
     205              :     // optional
     206            3 :     m.description = obj.value("description").toString();
     207            3 :     m.category = obj.value("category").toString();
     208            3 :     m.thumbnail = obj.value("thumbnail").toStdString();
     209            3 :     m.version = obj.value("version").toStdString();
     210            3 :     m.apiversion = obj.value("apiversion", DEFAULT_API_VERSION).toInt();
     211            3 :     const JsonValue contributes = obj.value("contributes");
     212            1 :     if (contributes.isObject()) {
     213            1 :         const JsonObject declared = contributes.toObject();
     214            3 :         for (const std::string& name : declared.keys()) {
     215            2 :             const JsonValue value = declared.value(name);
     216            2 :             if (!value.isArray()) {
     217            1 :                 continue;
     218              :             }
     219            1 :             std::vector<ValMap> contribution;
     220            1 :             const JsonArray items = value.toArray();
     221            1 :             contribution.reserve(items.size());
     222            2 :             for (size_t index = 0; index < items.size(); ++index) {
     223            1 :                 const JsonValue item = items.at(index);
     224            1 :                 if (item.isObject()) {
     225            1 :                     contribution.push_back(jsonMap(item.toObject()));
     226              :                 }
     227            1 :             }
     228            1 :             m.contributes.emplace(name, std::move(contribution));
     229            3 :         }
     230            1 :     }
     231              : 
     232              :     //! NOTE Default for actions
     233            3 :     m.context = obj.value("ui_context", std::string(DEFAULT_CONTEXT)).toStdString();
     234            3 :     const bool modal = obj.value("modal", DEFAULT_MODAL).toBool();
     235              : 
     236              :     // actions (required)
     237            1 :     ret = check_required(obj, "actions");
     238            1 :     if (!ret) {
     239            0 :         return RetVal<Manifest>::make_ret(ret);
     240              :     }
     241              : 
     242            3 :     const JsonArray arr = obj.value("actions").toArray();
     243            1 :     for (size_t i = 0; i < arr.size(); ++i) {
     244            0 :         const JsonObject ao = arr.at(i).toObject();
     245            0 :         Action a;
     246              :         // required
     247            0 :         a.path = ao.value("path").toStdString();
     248            0 :         if (a.path.empty()) {
     249            0 :             return RetVal<Manifest>::make_ret(Ret::Code::BadData,
     250              :                                               "action `path` is required but not specified"
     251            0 :                                               );
     252              :         }
     253              : 
     254              :         // optional
     255            0 :         a.code = ao.value("code", String::number(i)).toStdString();
     256            0 :         a.type = ao.contains("type") ? typeFromString(ao.value("type").toStdString()) : m.type;
     257            0 :         a.modal = ao.value("modal", modal).toBool();
     258            0 :         a.title = ao.value("title").toString();
     259            0 :         std::string icon = ao.value("icon").toStdString();
     260            0 :         a.icon = ui::IconCode::fromString(icon.c_str());
     261            0 :         a.context = ao.value("ui_context", m.context).toStdString();
     262            0 :         a.showOnAppmenu = ao.value("show_on_appmenu", true).toBool();
     263            0 :         a.showOnToolbar = ao.value("show_on_toolbar", false).toBool();
     264            0 :         a.func = ao.value("func", "main").toStdString();
     265            0 :         a.apiversion = m.apiversion;
     266            0 :         m.actions.push_back(std::move(a));
     267            0 :     }
     268              : 
     269            1 :     return RetVal<Manifest>::make_ok(m);
     270            1 : }
     271              : 
     272            0 : void ExtensionsLoader::resolvePaths(Manifest& m, const io::path_t& rootDirPath) const
     273              : {
     274            0 :     if (!m.thumbnail.empty()) {
     275            0 :         m.thumbnail = rootDirPath + "/" + m.thumbnail;
     276              :     }
     277              : 
     278            0 :     for (Action& a : m.actions) {
     279            0 :         a.path = rootDirPath + "/" + a.path;
     280              :     }
     281            0 : }
        

Generated by: LCOV version 2.0-1