LCOV - code coverage report
Current view: top level - muse/framework/extensions/internal/legacy - extpluginsloader.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 72.3 % 166 120
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 "extpluginsloader.h"
      23              : 
      24              : #include "global/io/dir.h"
      25              : #include "global/io/file.h"
      26              : #include "global/io/fileinfo.h"
      27              : #include "global/runtime.h"
      28              : 
      29              : #include "log.h"
      30              : 
      31              : const std::string DEV_PLUGINS("dev_test_");
      32              : 
      33              : using namespace muse;
      34              : using namespace muse::extensions;
      35              : using namespace muse::extensions::legacy;
      36              : 
      37            4 : static Uri makeUri(const io::path_t& rootPath, const io::path_t& path)
      38              : {
      39            4 :     std::string pathStr = path.toQString().sliced(rootPath.size()).toLower().toStdString();
      40            4 :     std::replace(pathStr.begin(), pathStr.end(), '.', '-');
      41            8 :     return Uri("extension://plugin/v1" + pathStr);
      42            4 : }
      43              : 
      44            0 : std::map<std::string /*codeKey*/, Uri> ExtPluginsLoader::loadCodekeyUriMap(const io::path_t& defPath, const io::path_t& extPath) const
      45              : {
      46            0 :     std::map<std::string /*codeKey*/, Uri> data;
      47              : 
      48            0 :     auto loadUris = [this](std::map<std::string /*codeKey*/, Uri>& data, const io::path_t& rootPath) {
      49            0 :         io::paths_t paths = qmlsPaths(rootPath);
      50            0 :         for (const io::path_t& path : paths) {
      51            0 :             std::string codeKey = io::completeBasename(path).toStdString();
      52            0 :             Uri uri = makeUri(rootPath, path);
      53            0 :             data.insert({ codeKey, uri });
      54            0 :         }
      55            0 :     };
      56              : 
      57            0 :     loadUris(data, defPath);
      58            0 :     loadUris(data, extPath);
      59              : 
      60              :     //! NOTE These plugins have been ported to extensions
      61            0 :     data["colornotes"] = Uri("extension://notation/colornotes");
      62            0 :     data["addCourtesyAccidentals"] = Uri("extension://notation/courtesy_accidentals");
      63              : 
      64            0 :     return data;
      65            0 : }
      66              : 
      67            4 : ManifestList ExtPluginsLoader::loadManifestList(const io::path_t& defPath, const io::path_t& extPath) const
      68              : {
      69            5 :     TRACEFUNC;
      70              : 
      71            4 :     LOGD() << "try load extensions, def: " << defPath << ", user: " << extPath;
      72              : 
      73            4 :     ManifestList defaultManifests = manifestList(defPath);
      74            4 :     ManifestList externalManifests = manifestList(extPath);
      75              : 
      76            4 :     ManifestList retList;
      77            8 :     for (const Manifest& m : defaultManifests) {
      78            4 :         if (!m.isValid()) {
      79            0 :             continue;
      80              :         }
      81            4 :         retList.push_back(m);
      82              :     }
      83              : 
      84            4 :     for (Manifest& m : externalManifests) {
      85            0 :         if (!m.isValid()) {
      86            0 :             continue;
      87              :         }
      88            0 :         m.isRemovable = true;
      89            0 :         retList.push_back(m);
      90              :     }
      91              : 
      92            4 :     return retList;
      93            4 : }
      94              : 
      95            8 : ManifestList ExtPluginsLoader::manifestList(const io::path_t& rootPath) const
      96              : {
      97            8 :     ManifestList manifests;
      98            8 :     io::paths_t paths = qmlsPaths(rootPath);
      99           12 :     for (const io::path_t& path : paths) {
     100            4 :         if (!runtime::isDebug() && (path.toStdString().find(DEV_PLUGINS) != std::string::npos)) {
     101            0 :             continue;
     102              :         }
     103              : 
     104            4 :         Manifest manifest = parseManifest(rootPath, path);
     105            4 :         if (!manifest.isValid()) {
     106            0 :             continue;
     107              :         }
     108            4 :         manifest.path = path;
     109            4 :         resolvePaths(manifest, io::FileInfo(path).dirPath());
     110            4 :         manifests.push_back(manifest);
     111            4 :     }
     112              : 
     113            8 :     return manifests;
     114            8 : }
     115              : 
     116            8 : io::paths_t ExtPluginsLoader::qmlsPaths(const io::path_t& rootPath) const
     117              : {
     118           16 :     RetVal<io::paths_t> paths = io::Dir::scanFiles(rootPath, { "*.qml" });
     119            8 :     if (!paths.ret) {
     120            0 :         LOGE() << "failed scan files, err: " << paths.ret.toString()
     121            0 :                << ", path: " << rootPath;
     122              :     }
     123              : 
     124           16 :     return paths.val;
     125            8 : }
     126              : 
     127            4 : Manifest ExtPluginsLoader::parseManifest(const io::path_t& rootPath, const io::path_t& path) const
     128              : {
     129            4 :     ByteArray data;
     130            4 :     Ret ret = io::File::readFile(path, data);
     131            4 :     if (!ret) {
     132            0 :         LOGE() << "failed read file: " << path << ", err: " << ret.toString();
     133            0 :         return Manifest();
     134              :     }
     135              : 
     136            4 :     String content = String::fromUtf8(data);
     137              : 
     138              :     //! NOTE Check is MuseScore plugin
     139              :     {
     140            4 :         size_t bracketPos = 0;
     141          128 :         for (size_t i = 0; i < content.size(); ++i) {
     142          128 :             if (content.at(i) == Char(u'{')) {
     143            4 :                 bracketPos = i;
     144            4 :                 break;
     145              :             }
     146              :         }
     147              : 
     148            4 :         if (bracketPos == 0) {
     149            0 :             LOGD() << "Not MuseScore plugin, path: " << path;
     150            0 :             return Manifest();
     151              :         }
     152              : 
     153            4 :         String header = content.left(bracketPos);
     154            4 :         if (!content.contains(u"MuseScore")) {
     155            0 :             LOGD() << "Not MuseScore plugin, path: " << path;
     156            0 :             return Manifest();
     157              :         }
     158            4 :     }
     159              : 
     160            4 :     io::FileInfo fi(path);
     161              : 
     162            4 :     Manifest m;
     163            4 :     m.uri = makeUri(rootPath, path);
     164            4 :     m.type = Type::Macros;
     165            4 :     m.apiversion = 1;
     166            4 :     m.legacyPlugin = true;
     167              : 
     168           12 :     auto dropQuotes = [](String str) {
     169           12 :         while (str.back() == ';') {
     170            0 :             str.truncate(str.size() - 1);
     171              :         }
     172           12 :         if (str.size() < 3) {
     173            1 :             return String();
     174              :         }
     175           11 :         if (str.startsWith(u"qsTr(\"") && str.endsWith(u"\")")) {
     176            0 :             return str.mid(6, str.size() - 8);
     177              :         }
     178              : 
     179           11 :         if (str.startsWith(u"\"") && str.endsWith(u"\"")) {
     180            7 :             return str.mid(1, str.size() - 2);
     181              :         }
     182              : 
     183            4 :         return str;
     184              :     };
     185              : 
     186            4 :     m.context = std::string(DEFAULT_CONTEXT);
     187            4 :     int needProperties = 7; // title, description, pluginType, category, thumbnail, requiresScore, version
     188            4 :     int propertiesFound = 0;
     189            4 :     bool docked = false;
     190            4 :     bool insideMuseScoreItem = false;
     191            4 :     size_t current, previous = 0;
     192            4 :     current = content.indexOf(u"\n");
     193              : 
     194              :     //! NOTE Supposed structure
     195              :     //! // comments
     196              :     //! import ...
     197              :     //! import ...
     198              :     //!
     199              :     //! MuseScore {
     200              :     //!
     201              :     //!     prop1: ...
     202              :     //!     prop2: ...
     203              :     //!     ...
     204              :     //!
     205              :     //!     As soon as the first open bracket is encountered,
     206              :     //!     we consider that the properties have ended.
     207              :     //!     Technically, there may be properties further down,
     208              :     //!     we just need to ask to move them up.
     209              :     //!
     210              :     //!     onRun: {..
     211              :     //!     function applyMirrorIntervals() {..
     212              :     //!     Item { ...
     213              :     //!
     214              : 
     215           28 :     while (current != std::string::npos) {
     216           24 :         String line = content.mid(previous, current - previous).trimmed();
     217              : 
     218              :         //! NOTE Needed for compatibility with 3.6
     219              :         //! We can add properties from 4.4 that are not in 3.6
     220           24 :         if (line.startsWith(u"//4.4 ")) {
     221            0 :             line = line.mid(6);
     222              :         }
     223              : 
     224           24 :         if (line.startsWith(u'/')) { // comment
     225              :             // noop
     226           24 :         } else if (line.endsWith(u'{')) {
     227            4 :             if (insideMuseScoreItem) {
     228            0 :                 break;
     229              :             }
     230              : 
     231            4 :             if (!insideMuseScoreItem) {
     232            4 :                 insideMuseScoreItem = true;
     233              :             }
     234           20 :         } else if (line.startsWith(u"title:")) {
     235            4 :             m.title = dropQuotes(line.mid(6).trimmed());
     236            4 :             ++propertiesFound;
     237           16 :         } else if (line.startsWith(u"description:")) {
     238            0 :             m.description = dropQuotes(line.mid(12).trimmed());
     239            0 :             ++propertiesFound;
     240           16 :         } else if (line.startsWith(u"pluginType:")) {
     241            4 :             String pluginType = dropQuotes(line.mid(11).trimmed());
     242            4 :             docked = pluginType == "dock";
     243            4 :             if (pluginType == "dialog" || pluginType == "dock") {
     244            2 :                 m.type = Type::Form;
     245              :             }
     246            4 :             ++propertiesFound;
     247           16 :         } else if (line.startsWith(u"categoryCode:")) {
     248            0 :             m.category = dropQuotes(line.mid(13).trimmed());
     249            0 :             ++propertiesFound;
     250           12 :         } else if (line.startsWith(u"thumbnailName:")) {
     251            0 :             m.thumbnail = dropQuotes(line.mid(14).trimmed()).toStdString();
     252            0 :             ++propertiesFound;
     253           12 :         } else if (line.startsWith(u"requiresScore:")) {
     254            4 :             String requiresScore = dropQuotes(line.mid(14).trimmed());
     255            4 :             if (requiresScore == u"false") {
     256            4 :                 m.context = "Any";
     257              :             }
     258            4 :             ++propertiesFound;
     259           12 :         } else if (line.startsWith(u"version:")) {
     260            0 :             m.version = dropQuotes(line.mid(8).trimmed()).toStdString();
     261            0 :             ++propertiesFound;
     262              :         }
     263              : 
     264           24 :         if (propertiesFound == needProperties) {
     265            0 :             break;
     266              :         }
     267              : 
     268           24 :         previous = current + 1;
     269           24 :         current = content.indexOf(u"\n", previous);
     270           24 :     }
     271              : 
     272            4 :     if (m.title.isEmpty()) {
     273            0 :         m.title = fi.baseName();
     274              :     }
     275              : 
     276            4 :     Action a;
     277            4 :     if (docked) {
     278            2 :         m.context = std::string(PROJECT_OPENED_CONTEXT);
     279              :     }
     280            4 :     a.code = "main";
     281            4 :     a.type = m.type;
     282            4 :     a.docked = docked;
     283            4 :     a.title = m.title;
     284            4 :     a.context = m.context;
     285            4 :     a.apiversion = m.apiversion;
     286            4 :     a.legacyPlugin = m.legacyPlugin;
     287            4 :     a.path = fi.fileName();
     288            4 :     m.actions.push_back(std::move(a));
     289              : 
     290            4 :     return m;
     291            4 : }
     292              : 
     293            4 : void ExtPluginsLoader::resolvePaths(Manifest& m, const io::path_t& rootDirPath) const
     294              : {
     295            4 :     if (!m.thumbnail.empty()) {
     296            0 :         m.thumbnail = rootDirPath + "/" + m.thumbnail;
     297              :     }
     298              : 
     299            8 :     for (Action& a : m.actions) {
     300            4 :         a.path = rootDirPath + "/" + a.path;
     301              :     }
     302            4 : }
        

Generated by: LCOV version 2.0-1