LCOV - code coverage report
Current view: top level - muse/framework/audioplugins/internal - knownaudiopluginsregister.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 64.7 % 275 178
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) 2021 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              : 
      23              : #include "knownaudiopluginsregister.h"
      24              : 
      25              : #include "global/serialization/json.h"
      26              : 
      27              : #include "log.h"
      28              : 
      29              : using namespace muse;
      30              : using namespace muse::audioplugins;
      31              : 
      32              : namespace muse::audioplugins {
      33           23 : static JsonObject attributesToJson(const PluginAttributes& attributes,
      34              :                                    const PluginAttributes& runtimeOnly)
      35              : {
      36           23 :     JsonObject result;
      37              : 
      38           41 :     for (auto it = attributes.cbegin(); it != attributes.cend(); ++it) {
      39           18 :         if (runtimeOnly.find(it->first) != runtimeOnly.cend()) {
      40            8 :             continue;
      41              :         }
      42              : 
      43           10 :         result.set(it->first.toStdString(), it->second.toStdString());
      44              :     }
      45              : 
      46           23 :     return result;
      47            0 : }
      48              : 
      49           23 : static JsonObject metaToJson(const PluginMeta& meta, const PluginAttributes& runtimeOnly)
      50              : {
      51           23 :     JsonObject result;
      52              : 
      53           46 :     result.set("id", meta.id);
      54           46 :     result.set("type", meta.type);
      55              : 
      56           23 :     if (!meta.vendor.empty()) {
      57           15 :         result.set("vendor", meta.vendor);
      58              :     }
      59              : 
      60           23 :     JsonObject attributesJson = attributesToJson(meta.attributes, runtimeOnly);
      61           23 :     if (!attributesJson.empty()) {
      62           16 :         result.set("attributes", attributesJson);
      63              :     }
      64              : 
      65           23 :     return result;
      66           23 : }
      67              : 
      68            3 : static PluginAttributes attributesFromJson(const JsonObject& object)
      69              : {
      70            3 :     PluginAttributes result;
      71              : 
      72            7 :     for (const std::string& key : object.keys()) {
      73            4 :         result.insert({ String::fromStdString(key), object.value(key).toString() });
      74            3 :     }
      75              : 
      76            3 :     return result;
      77            0 : }
      78              : 
      79            6 : static PluginMeta metaFromJson(const JsonObject& object)
      80              : {
      81            6 :     PluginMeta result;
      82              : 
      83           18 :     result.id = object.value("id").toStdString();
      84           18 :     result.type = object.value("type").toStdString();
      85           18 :     result.vendor = object.value("vendor").toStdString();
      86              : 
      87           18 :     JsonValue attributes = object.value("attributes");
      88            6 :     if (attributes.isObject()) {
      89            3 :         result.attributes = attributesFromJson(attributes.toObject());
      90              :     }
      91              : 
      92            6 :     return result;
      93            6 : }
      94              : }
      95              : 
      96            8 : Ret KnownAudioPluginsRegister::load()
      97              : {
      98            9 :     TRACEFUNC;
      99              : 
     100            8 :     m_loaded = false;
     101            8 :     m_pluginInfoMap.clear();
     102            8 :     m_pluginPaths.clear();
     103              : 
     104            8 :     io::path_t knownAudioPluginsPath = configuration()->knownAudioPluginsFilePath();
     105            8 :     if (!fileSystem()->exists(knownAudioPluginsPath)) {
     106            3 :         m_loaded = true;
     107            3 :         return muse::make_ok();
     108              :     }
     109              : 
     110            5 :     RetVal<ByteArray> file = fileSystem()->readFile(knownAudioPluginsPath);
     111            5 :     if (!file.ret) {
     112            0 :         LOGE() << "Failed to read known-audio-plugins cache "
     113            0 :                << knownAudioPluginsPath << ": " << file.ret.toString();
     114            0 :         return file.ret;
     115              :     }
     116              : 
     117            5 :     std::string err;
     118            5 :     JsonDocument json = JsonDocument::fromJson(file.val, &err);
     119            5 :     if (!err.empty()) {
     120            0 :         LOGE() << "Failed to parse known-audio-plugins cache "
     121            0 :                << knownAudioPluginsPath << ": " << err;
     122            0 :         return Ret(static_cast<int>(Ret::Code::UnknownError), err);
     123              :     }
     124              : 
     125            5 :     JsonArray array;
     126            5 :     int fileVersion = 0;
     127              : 
     128            5 :     if (json.isArray()) {
     129              :         // legacy format: bare array, treated as version 0
     130            1 :         array = json.rootArray();
     131            4 :     } else if (json.isObject()) {
     132            4 :         JsonObject root = json.rootObject();
     133           12 :         const JsonValue versionVal = root.value("version");
     134           12 :         const JsonValue pluginsVal = root.value("plugins");
     135            4 :         if (!versionVal.isNumber() || !pluginsVal.isArray()) {
     136            0 :             LOGE() << "Malformed known-audio-plugins.json envelope at "
     137            0 :                    << knownAudioPluginsPath << " (expected numeric \"version\" and array \"plugins\")";
     138            0 :             return Ret(static_cast<int>(Ret::Code::UnknownError), "Malformed known_audio_plugins.json envelope");
     139              :         }
     140            4 :         fileVersion = versionVal.toInt();
     141            4 :         array = pluginsVal.toArray();
     142            4 :     } else {
     143            0 :         LOGE() << "Unrecognized known-audio-plugins.json root type at "
     144            0 :                << knownAudioPluginsPath << " (expected array or object)";
     145            0 :         return Ret(static_cast<int>(Ret::Code::UnknownError), "Unrecognized known_audio_plugins.json root type");
     146              :     }
     147              : 
     148            5 :     if (fileVersion > CURRENT_KNOWN_AUDIO_PLUGINS_VERSION) {
     149            2 :         LOGW() << "known-audio-plugins cache is newer (v" << fileVersion << ") than this build (v"
     150            1 :                << CURRENT_KNOWN_AUDIO_PLUGINS_VERSION << "); resetting it";
     151            1 :         m_ignoredEntries.clear();
     152            1 :         m_loaded = true;
     153            1 :         pluginInfoListChanged().notify();
     154            1 :         return muse::make_ok();
     155              :     }
     156              : 
     157            4 :     Ret migrationRet = migrations()->migrate(fileVersion, CURRENT_KNOWN_AUDIO_PLUGINS_VERSION, array);
     158            4 :     if (!migrationRet) {
     159            2 :         LOGE() << "Failed to migrate known-audio-plugins cache from v" << fileVersion
     160            1 :                << " to v" << CURRENT_KNOWN_AUDIO_PLUGINS_VERSION
     161            1 :                << ": " << migrationRet.toString();
     162            1 :         return migrationRet;
     163              :     }
     164              : 
     165            3 :     m_ignoredEntries.clear();
     166              : 
     167            9 :     for (size_t i = 0; i < array.size(); ++i) {
     168            6 :         JsonObject object = array.at(i).toObject();
     169              : 
     170            6 :         AudioPluginInfo info;
     171           18 :         info.meta = metaFromJson(object.value("meta").toObject());
     172           12 :         for (const auto& kv : configuration()->runtimeAttributeDefaults()) {
     173              :             // assign, not emplace: defaults must override stale values from a legacy cache
     174            6 :             info.meta.attributes[kv.first] = kv.second;
     175              :         }
     176           18 :         const io::path_t storedPath = object.value("path").toString();
     177            6 :         info.path = globalConfiguration()->isBundledPath(storedPath)
     178           12 :                     ? globalConfiguration()->fromBundledPath(storedPath)
     179            6 :                     : storedPath;
     180           18 :         info.state = audioPluginStateFromName(object.value("state").toStdString());
     181           18 :         info.errorCode = object.value("errorCode").toInt();
     182              : 
     183              :         // id and path are the lookup keys; skip just the malformed row,
     184              :         // aborting the whole load would trip asserts in the next scan
     185            6 :         if (info.meta.id.empty() || info.path.empty()) {
     186            2 :             LOGW() << "Skipping malformed known-audio-plugins entry at "
     187            1 :                    << knownAudioPluginsPath << " (missing id or path)";
     188            1 :             continue;
     189              :         }
     190              : 
     191              :         // a bundled plugin not found in this app may belong to another installed
     192              :         // instance; keep its record but don't expose it
     193            5 :         if (globalConfiguration()->isBundledPath(storedPath) && !fileSystem()->exists(info.path)) {
     194            0 :             m_ignoredEntries.push_back(object);
     195            0 :             continue;
     196              :         }
     197              : 
     198            5 :         m_pluginPaths.insert(info.path);
     199            5 :         m_pluginInfoMap.emplace(info.meta.id, std::move(info));
     200            8 :     }
     201              : 
     202            3 :     m_loaded = true;
     203            3 :     pluginInfoListChanged().notify();
     204            3 :     return muse::make_ok();
     205            8 : }
     206              : 
     207            0 : Ret KnownAudioPluginsRegister::clear()
     208              : {
     209            0 :     m_pluginInfoMap.clear();
     210            0 :     m_pluginPaths.clear();
     211              : 
     212            0 :     Ret ret = writePluginsInfo();
     213            0 :     m_pluginInfoListChanged.notify();
     214              : 
     215            0 :     return ret;
     216            0 : }
     217              : 
     218            9 : AudioPluginInfoList KnownAudioPluginsRegister::pluginInfoList(PluginInfoAccepted accepted) const
     219              : {
     220            9 :     if (!accepted) {
     221            9 :         return muse::values(m_pluginInfoMap);
     222              :     }
     223              : 
     224            0 :     AudioPluginInfoList result;
     225            0 :     result.reserve(m_pluginInfoMap.size());
     226              : 
     227            0 :     for (auto it = m_pluginInfoMap.cbegin(); it != m_pluginInfoMap.cend(); ++it) {
     228            0 :         if (accepted(it->second)) {
     229            0 :             result.push_back(it->second);
     230              :         }
     231              :     }
     232              : 
     233            0 :     return result;
     234            0 : }
     235              : 
     236            4 : muse::async::Notification KnownAudioPluginsRegister::pluginInfoListChanged() const
     237              : {
     238            4 :     return m_pluginInfoListChanged;
     239              : }
     240              : 
     241            4 : const io::path_t& KnownAudioPluginsRegister::pluginPath(const PluginResourceId& resourceId) const
     242              : {
     243            4 :     auto it = m_pluginInfoMap.find(resourceId);
     244            4 :     if (it == m_pluginInfoMap.end()) {
     245            1 :         static const io::path_t _dummy;
     246            1 :         return _dummy;
     247              :     }
     248              : 
     249            3 :     return it->second.path;
     250              : }
     251              : 
     252           10 : bool KnownAudioPluginsRegister::exists(const io::path_t& pluginPath) const
     253              : {
     254           10 :     return muse::contains(m_pluginPaths, pluginPath);
     255              : }
     256              : 
     257           12 : bool KnownAudioPluginsRegister::exists(const PluginResourceId& resourceId) const
     258              : {
     259           12 :     return muse::contains(m_pluginInfoMap, resourceId);
     260              : }
     261              : 
     262            7 : Ret KnownAudioPluginsRegister::registerPlugins(const AudioPluginInfoList& list)
     263              : {
     264            7 :     IF_ASSERT_FAILED(m_loaded) {
     265            0 :         return false;
     266              :     }
     267              : 
     268            7 :     if (list.empty()) {
     269            0 :         return make_ok();
     270              :     }
     271              : 
     272            7 :     bool changed = false;
     273              : 
     274           16 :     for (const AudioPluginInfo& info : list) {
     275            9 :         auto it = m_pluginInfoMap.find(info.meta.id);
     276            9 :         if (it != m_pluginInfoMap.end()) {
     277            4 :             if (it->second.path == info.path) {
     278            1 :                 LOGW() << "Plugin is already registered: " << info.path << ", ID: " << info.meta.id;
     279            1 :                 continue;
     280              :             }
     281              :         }
     282              : 
     283            8 :         m_pluginInfoMap.emplace(info.meta.id, info);
     284            8 :         m_pluginPaths.insert(info.path);
     285            8 :         changed = true;
     286              :     }
     287              : 
     288            7 :     if (changed) {
     289            6 :         return writePluginsInfo();
     290              :     }
     291              : 
     292            1 :     return make_ok();
     293              : }
     294              : 
     295            1 : Ret KnownAudioPluginsRegister::setPluginsState(const io::paths_t& paths, AudioPluginState state)
     296              : {
     297            1 :     IF_ASSERT_FAILED(m_loaded) {
     298            0 :         return false;
     299              :     }
     300              : 
     301            1 :     if (paths.empty()) {
     302            0 :         return make_ok();
     303              :     }
     304              : 
     305            1 :     bool changed = false;
     306            4 :     for (auto it = m_pluginInfoMap.begin(); it != m_pluginInfoMap.end(); ++it) {
     307            3 :         if (it->second.state != state && muse::contains(paths, it->second.path)) {
     308            2 :             it->second.state = state;
     309            2 :             changed = true;
     310              :         }
     311              :     }
     312              : 
     313            1 :     if (!changed) {
     314            0 :         return make_ok();
     315              :     }
     316              : 
     317            1 :     return writePluginsInfo();
     318              : }
     319              : 
     320            1 : Ret KnownAudioPluginsRegister::unregisterPlugins(const PluginResourceIdList& resourceIds)
     321              : {
     322            1 :     IF_ASSERT_FAILED(m_loaded) {
     323            0 :         return false;
     324              :     }
     325              : 
     326            1 :     if (resourceIds.empty()) {
     327            0 :         return make_ok();
     328              :     }
     329              : 
     330            1 :     bool changed = false;
     331              : 
     332            2 :     for (const PluginResourceId& resourceId : resourceIds) {
     333            1 :         if (!exists(resourceId)) {
     334            0 :             continue;
     335              :         }
     336              : 
     337            6 :         for (const auto& pair : m_pluginInfoMap) {
     338            5 :             if (pair.first == resourceId) {
     339            1 :                 muse::remove(m_pluginPaths, pair.second.path);
     340              :             }
     341              :         }
     342              : 
     343            1 :         m_pluginInfoMap.erase(resourceId);
     344            1 :         changed = true;
     345              :     }
     346              : 
     347            1 :     if (changed) {
     348            1 :         return writePluginsInfo();
     349              :     }
     350              : 
     351            0 :     return make_ok();
     352              : }
     353              : 
     354            0 : Ret KnownAudioPluginsRegister::removePluginsAtPath(const io::path_t& path)
     355              : {
     356            0 :     IF_ASSERT_FAILED(m_loaded) {
     357            0 :         return false;
     358              :     }
     359              : 
     360            0 :     bool removed = false;
     361            0 :     for (auto it = m_pluginInfoMap.begin(); it != m_pluginInfoMap.end();) {
     362            0 :         if (it->second.path == path) {
     363            0 :             it = m_pluginInfoMap.erase(it);
     364            0 :             removed = true;
     365              :         } else {
     366            0 :             ++it;
     367              :         }
     368              :     }
     369              : 
     370            0 :     if (!removed) {
     371            0 :         return make_ok();
     372              :     }
     373              : 
     374            0 :     muse::remove(m_pluginPaths, path);
     375              : 
     376            0 :     return writePluginsInfo();
     377              : }
     378              : 
     379            8 : Ret KnownAudioPluginsRegister::writePluginsInfo()
     380              : {
     381            9 :     TRACEFUNC;
     382              : 
     383            8 :     JsonArray array;
     384              : 
     385            8 :     const PluginAttributes& runtimeOnly = configuration()->runtimeAttributeDefaults();
     386              : 
     387           31 :     for (const auto& pair : m_pluginInfoMap) {
     388           23 :         const AudioPluginInfo& info = pair.second;
     389              : 
     390           23 :         const io::path_t storedPath = globalConfiguration()->isBundledWithApp(info.path)
     391           23 :                                       ? globalConfiguration()->toBundledPath(info.path)
     392           23 :                                       : info.path;
     393              : 
     394           23 :         JsonObject obj;
     395           69 :         obj.set("meta", metaToJson(info.meta, runtimeOnly));
     396           46 :         obj.set("path", storedPath.toStdString());
     397              : 
     398              :         // load() reads a missing "state" back as Undefined, so omitting it round-trips
     399           23 :         if (info.state != AudioPluginState::Undefined) {
     400           69 :             obj.set("state", audioPluginStateName(info.state));
     401              :         }
     402              : 
     403           23 :         if (info.errorCode != 0) {
     404            9 :             obj.set("errorCode", info.errorCode);
     405              :         }
     406              : 
     407           23 :         array << obj;
     408           23 :     }
     409              : 
     410              :     // write ignored records back untouched to preserve other instances' settings
     411            8 :     for (const JsonObject& ignored : m_ignoredEntries) {
     412            0 :         array << ignored;
     413              :     }
     414              : 
     415            8 :     JsonObject root;
     416           16 :     root.set("version", CURRENT_KNOWN_AUDIO_PLUGINS_VERSION);
     417            8 :     root.set("plugins", array);
     418              : 
     419            8 :     io::path_t knownAudioPluginsPath = configuration()->knownAudioPluginsFilePath();
     420            8 :     Ret ret = fileSystem()->writeFile(knownAudioPluginsPath, JsonDocument(root).toJson());
     421              : 
     422            8 :     return ret;
     423            8 : }
     424              : 
     425            0 : Ret KnownAudioPluginsRegister::writePluginsTo(const io::path_t& file, const AudioPluginInfoList& list) const
     426              : {
     427            0 :     TRACEFUNC;
     428              : 
     429            0 :     JsonArray array;
     430            0 :     const PluginAttributes& runtimeOnly = configuration()->runtimeAttributeDefaults();
     431              : 
     432            0 :     for (const AudioPluginInfo& info : list) {
     433            0 :         JsonObject obj;
     434            0 :         obj.set("meta", metaToJson(info.meta, runtimeOnly));
     435            0 :         obj.set("path", info.path.toStdString());
     436              : 
     437            0 :         if (info.state != AudioPluginState::Undefined) {
     438            0 :             obj.set("state", audioPluginStateName(info.state));
     439              :         }
     440              : 
     441            0 :         if (info.errorCode != 0) {
     442            0 :             obj.set("errorCode", info.errorCode);
     443              :         }
     444              : 
     445            0 :         array << obj;
     446            0 :     }
     447              : 
     448            0 :     JsonObject root;
     449            0 :     root.set("version", CURRENT_KNOWN_AUDIO_PLUGINS_VERSION);
     450            0 :     root.set("plugins", array);
     451              : 
     452            0 :     return fileSystem()->writeFile(file, JsonDocument(root).toJson());
     453            0 : }
     454              : 
     455            0 : RetVal<AudioPluginInfoList> KnownAudioPluginsRegister::readPluginsFrom(const io::path_t& file) const
     456              : {
     457            0 :     TRACEFUNC;
     458              : 
     459            0 :     RetVal<ByteArray> bytes = fileSystem()->readFile(file);
     460            0 :     if (!bytes.ret) {
     461            0 :         return RetVal<AudioPluginInfoList>(bytes.ret);
     462              :     }
     463              : 
     464            0 :     std::string err;
     465            0 :     JsonDocument json = JsonDocument::fromJson(bytes.val, &err);
     466            0 :     if (!err.empty()) {
     467            0 :         return RetVal<AudioPluginInfoList>(Ret(static_cast<int>(Ret::Code::UnknownError), err));
     468              :     }
     469              : 
     470            0 :     JsonArray array = json.rootObject().value("plugins").toArray();
     471              : 
     472            0 :     AudioPluginInfoList result;
     473            0 :     result.reserve(array.size());
     474              : 
     475            0 :     for (size_t i = 0; i < array.size(); ++i) {
     476            0 :         JsonObject object = array.at(i).toObject();
     477              : 
     478            0 :         AudioPluginInfo info;
     479            0 :         info.meta = metaFromJson(object.value("meta").toObject());
     480            0 :         info.path = object.value("path").toString();
     481            0 :         info.state = audioPluginStateFromName(object.value("state").toStdString());
     482            0 :         info.errorCode = object.value("errorCode").toInt();
     483              : 
     484            0 :         if (info.meta.id.empty() || info.path.empty()) {
     485            0 :             continue;
     486              :         }
     487              : 
     488            0 :         result.push_back(std::move(info));
     489            0 :     }
     490              : 
     491            0 :     return RetVal<AudioPluginInfoList>::make_ok(result);
     492            0 : }
        

Generated by: LCOV version 2.0-1