LCOV - code coverage report
Current view: top level - src/engraving/api/v1 - util.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 39.3 % 178 70
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-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 <QDateTime>
      24              : #include <QFileInfo>
      25              : #include <QQmlEngine>
      26              : #include <QQmlContext>
      27              : 
      28              : #include "project/inotationproject.h"
      29              : 
      30              : #include "util.h"
      31              : 
      32              : #include "log.h"
      33              : 
      34              : using namespace muse;
      35              : 
      36              : namespace mu::engraving::apiv1 {
      37              : //---------------------------------------------------------
      38              : //   FileIO
      39              : //---------------------------------------------------------
      40              : 
      41            0 : FileIO::FileIO(QObject* parent)
      42            0 :     : QObject(parent), muse::Contextable(muse::iocCtxForQmlObject(this))
      43              : {
      44            0 : }
      45              : 
      46            4 : FileIO::FileIO(const muse::modularity::ContextPtr& ctx, QObject* parent)
      47            4 :     : QObject(parent), muse::Contextable(ctx)
      48              : {
      49            4 : }
      50              : 
      51              : // User's MuseScore documents directory (default location for scores, plugins, etc.)
      52            8 : QString FileIO::userDataPath()
      53              : {
      54            8 :     return globalConfiguration() ? globalConfiguration()->userDataPath().toQString() : QString();
      55              : }
      56              : 
      57              : // User-configured Plugins directory (Preferences → Folders → Plugins)
      58            8 : QString FileIO::pluginsUserPath()
      59              : {
      60            8 :     return extensionsConfiguration() ? extensionsConfiguration()->pluginsUserPath().toQString() : QString();
      61              : }
      62              : 
      63              : // User-configured Scores directory (Preferences → Folders → Scores)
      64            8 : QString FileIO::userProjectsPath()
      65              : {
      66            8 :     return projectConfiguration() ? projectConfiguration()->userProjectsPath().toQString() : QString();
      67              : }
      68              : 
      69              : // User-configured Templates directory (Preferences → Folders → Templates)
      70            8 : QString FileIO::userTemplatesPath()
      71              : {
      72            8 :     return projectConfiguration() ? projectConfiguration()->userTemplatesPath().toQString() : QString();
      73              : }
      74              : 
      75              : // User-configured Styles directory (Preferences → Folders → Styles)
      76            8 : QString FileIO::userStylesPath()
      77              : {
      78            8 :     return notationConfiguration() ? notationConfiguration()->userStylesPath().toQString() : QString();
      79              : }
      80              : 
      81              : // User-configured SoundFonts directories (Preferences → Folders → SoundFonts)
      82            8 : QStringList FileIO::userSoundFontDirectories()
      83              : {
      84            8 :     QStringList paths;
      85            8 :     if (!audioConfiguration()) {
      86            8 :         return paths;
      87              :     }
      88              : 
      89            0 :     for (const auto& path : audioConfiguration()->userSoundFontDirectories()) {
      90            0 :         if (!path.empty()) {
      91            0 :             paths << path.toQString();
      92              :         }
      93            0 :     }
      94              : 
      95            0 :     return paths;
      96            0 : }
      97              : 
      98              : // The running plugin's directory
      99            0 : QString FileIO::pluginDirectoryPath()
     100              : {
     101            0 :     QQmlContext* context = QQmlEngine::contextForObject(this);
     102              : 
     103            0 :     if (!context) {
     104            0 :         return QString();
     105              :     }
     106              : 
     107            0 :     QUrl url = context->baseUrl();
     108              : 
     109            0 :     if (!url.isLocalFile()) {
     110            0 :         return QString();
     111              :     }
     112              : 
     113            0 :     return QFileInfo(url.toLocalFile()).absolutePath();
     114            0 : }
     115              : 
     116              : // Path of project file (ex: .../Desktop/project.mscz)
     117            8 : QString FileIO::projectPath()
     118              : {
     119            8 :     if (!projectSession()) {
     120            8 :         return QString();
     121              :     }
     122              : 
     123            0 :     project::INotationProjectPtr project = projectSession()->project();
     124            0 :     if (!project) {
     125            0 :         return QString();
     126              :     }
     127              : 
     128            0 :     muse::io::path_t projectPath = project->path();
     129            0 :     if (projectPath.empty()) {
     130            0 :         return QString();
     131              :     }
     132              : 
     133            0 :     return projectPath.toQString();
     134            0 : }
     135              : 
     136              : // Is the project a folder with a .mscx file
     137            8 : bool FileIO::isProjectDirectory()
     138              : {
     139            8 :     QString projectPath = FileIO::projectPath();
     140            8 :     if (projectPath.isEmpty()) {
     141            8 :         return false;
     142              :     }
     143              : 
     144            0 :     std::string suffix = muse::io::suffix(projectPath);
     145              : 
     146            0 :     return mscIoModeBySuffix(suffix) == MscIoMode::Dir;
     147            8 : }
     148              : 
     149              : // Path of project's containing folder
     150            0 : QString FileIO::projectDirectoryPath()
     151              : {
     152            0 :     QString projectPath = FileIO::projectPath();
     153            0 :     if (projectPath.isEmpty()) {
     154            0 :         return QString();
     155              :     }
     156              : 
     157            0 :     QString directoryPath = io::dirpath(projectPath).toQString();
     158            0 :     return directoryPath;
     159            0 : }
     160              : 
     161              : //---------------------------------------------------------
     162              : //   isPathWriteable
     163              : //   Check if the file path is within allowed directories
     164              : //---------------------------------------------------------
     165            8 : bool FileIO::isPathWriteable(const QString& filePath)
     166              : {
     167              :     // Get the canonical (absolute, symlinks resolved) path
     168            8 :     QFileInfo fileInfo(filePath);
     169            8 :     QString canonicalPath = fileInfo.canonicalFilePath();
     170              : 
     171              :     // If file doesn't exist yet, we need to clean the path manually
     172              :     // because absoluteFilePath() does NOT resolve .. and . elements
     173            8 :     if (canonicalPath.isEmpty()) {
     174              :         // Use QDir::cleanPath to resolve .. and . elements
     175            8 :         canonicalPath = QDir::cleanPath(fileInfo.absoluteFilePath());
     176              :     }
     177              : 
     178              :     // Build list of allowed base paths from all user-configurable directories
     179            8 :     QStringList allowedPaths;
     180              : 
     181              :     // Note: userAppDataPath() is NOT included because it contains sensitive data:
     182              :     // - User credentials (musescorecom_cred.dat)
     183              :     // - System configuration (shortcuts.xml, midi_mappings.xml)
     184              :     // - Application logs
     185              :     // Plugins should not write to this directory
     186              : 
     187              :     // 1. System temp directory (for temporary files)
     188            8 :     QDir tempDir;
     189            8 :     allowedPaths << tempDir.tempPath();
     190              : 
     191              :     // 2. User's MuseScore documents directory (default location for Scores, Plugins, SoundFonts, Styles, Templates)
     192            8 :     if (QString path = FileIO::userDataPath(); !path.isEmpty()) {
     193            8 :         allowedPaths << path;
     194            8 :     }
     195              : 
     196              :     // 3. User-configured Plugins directory (Preferences → Folders → Plugins)
     197            8 :     if (QString path = FileIO::pluginsUserPath(); !path.isEmpty()) {
     198            0 :         allowedPaths << path;
     199            8 :     }
     200              : 
     201              :     // 4. User-configured Scores directory (Preferences → Folders → Scores)
     202            8 :     if (QString path = FileIO::userProjectsPath(); !path.isEmpty()) {
     203            0 :         allowedPaths << path;
     204            8 :     }
     205              : 
     206              :     // 5. User-configured Templates directory (Preferences → Folders → Templates)
     207            8 :     if (QString path = FileIO::userTemplatesPath(); !path.isEmpty()) {
     208            0 :         allowedPaths << path;
     209            8 :     }
     210              : 
     211              :     // 6. User-configured Styles directory (Preferences → Folders → Styles)
     212            8 :     if (QString path = FileIO::userStylesPath(); !path.isEmpty()) {
     213            0 :         allowedPaths << path;
     214            8 :     }
     215              : 
     216              :     // 7. User-configured SoundFonts directories (Preferences → Folders → SoundFonts)
     217            8 :     allowedPaths << FileIO::userSoundFontDirectories();
     218              : 
     219              :     // 8. Project path if it's a folder (with a .mscx file) instead of a .mscz file
     220            8 :     if (FileIO::isProjectDirectory()) {
     221            0 :         allowedPaths << FileIO::projectDirectoryPath();
     222              :     }
     223              : 
     224              :     // Check if the canonical path starts with any allowed base path
     225            8 :     bool allowed = false;
     226           20 :     for (const QString& basePath : allowedPaths) {
     227           14 :         if (canonicalPath.startsWith(basePath)) {
     228            2 :             allowed = true;
     229            2 :             break;
     230              :         }
     231              :     }
     232              : 
     233            8 :     if (!allowed) {
     234            6 :         LOGW() << "File write blocked: path '" << canonicalPath << "' is outside allowed directories";
     235            6 :         LOGW() << "Allowed directories: " << allowedPaths.join(", ");
     236              :     }
     237              : 
     238            8 :     return allowed;
     239            8 : }
     240              : 
     241            0 : QString FileIO::read()
     242              : {
     243            0 :     if (m_source.isEmpty()) {
     244            0 :         emit error("source is empty");
     245            0 :         return QString();
     246              :     }
     247            0 :     QUrl url(m_source);
     248            0 :     QString source(m_source);
     249            0 :     if (url.isValid() && url.isLocalFile()) {
     250            0 :         source = url.toLocalFile();
     251              :     }
     252            0 :     QFile file(source);
     253            0 :     QString fileContent;
     254            0 :     if (file.open(QIODevice::ReadOnly)) {
     255            0 :         QString line;
     256            0 :         QTextStream t(&file);
     257              :         do {
     258            0 :             line = t.readLine();
     259            0 :             fileContent += line + "\n";
     260            0 :         } while (!line.isNull());
     261            0 :         file.close();
     262            0 :     } else {
     263            0 :         emit error("Unable to open the file");
     264            0 :         return QString();
     265              :     }
     266            0 :     return fileContent;
     267            0 : }
     268              : 
     269            8 : bool FileIO::write(const QString& data)
     270              : {
     271            8 :     if (m_source.isEmpty()) {
     272            0 :         return false;
     273              :     }
     274              : 
     275            8 :     QUrl url(m_source);
     276              : 
     277            8 :     QString source = (url.isValid() && url.isLocalFile()) ? url.toLocalFile() : m_source;
     278              : 
     279              :     // Security: Check if path is within allowed directories
     280            8 :     if (!FileIO::isPathWriteable(source)) {
     281            6 :         emit error("File write blocked: path is outside allowed directories");
     282            6 :         return false;
     283              :     }
     284              : 
     285            2 :     QFile file(source);
     286            2 :     if (!file.open(QFile::WriteOnly | QFile::Truncate)) {
     287            0 :         return false;
     288              :     }
     289              : 
     290            2 :     QTextStream out(&file);
     291            2 :     out << data;
     292            2 :     file.close();
     293            2 :     return true;
     294            8 : }
     295              : 
     296            0 : bool FileIO::writeBinary(const QString& data)
     297              : {
     298            0 :     if (m_source.isEmpty()) {
     299            0 :         return false;
     300              :     }
     301              : 
     302            0 :     QUrl url(m_source);
     303              : 
     304            0 :     QString source = (url.isValid() && url.isLocalFile()) ? url.toLocalFile() : m_source;
     305              : 
     306              :     // Security: Check if path is within allowed directories
     307            0 :     if (!FileIO::isPathWriteable(source)) {
     308            0 :         emit error("File write blocked: path is outside allowed directories");
     309            0 :         return false;
     310              :     }
     311              : 
     312            0 :     QFile file(source);
     313            0 :     if (!file.open(QFile::WriteOnly | QFile::Truncate)) {
     314            0 :         return false;
     315              :     }
     316              : 
     317              :     // Write binary data without text encoding
     318              :     // Each character in the QString represents a byte (0-255)
     319            0 :     QByteArray bytes = data.toLatin1();
     320            0 :     qint64 written = file.write(bytes);
     321            0 :     file.close();
     322              : 
     323            0 :     return written == bytes.size();
     324            0 : }
     325              : 
     326              : //---------------------------------------------------------
     327              : //   remove
     328              : //---------------------------------------------------------
     329              : 
     330            0 : bool FileIO::remove()
     331              : {
     332            0 :     if (m_source.isEmpty()) {
     333            0 :         return false;
     334              :     }
     335              : 
     336            0 :     QFile file(m_source);
     337            0 :     return file.remove();
     338            0 : }
     339              : 
     340            0 : bool FileIO::exists()
     341              : {
     342            0 :     QFile file(m_source);
     343            0 :     return file.exists();
     344            0 : }
     345              : 
     346            0 : int FileIO::modifiedTime()
     347              : {
     348            0 :     if (m_source.isEmpty()) {
     349            0 :         emit error("source is empty");
     350            0 :         return 0;
     351              :     }
     352            0 :     QUrl url(m_source);
     353            0 :     QString source(m_source);
     354            0 :     if (url.isValid() && url.isLocalFile()) {
     355            0 :         source = url.toLocalFile();
     356              :     }
     357            0 :     QFileInfo fileInfo(source);
     358            0 :     return fileInfo.lastModified().toSecsSinceEpoch();
     359            0 : }
     360              : 
     361            0 : void MsProcess::start(const QString& command)
     362              : {
     363              :     QT_WARNING_PUSH;
     364              :     QT_WARNING_DISABLE_DEPRECATED;
     365            0 :     DEPRECATED_USE("startWithArgs(program, [arg1, arg2, ...])");
     366            0 :     QProcess::start(command);
     367              :     QT_WARNING_POP;
     368            0 : }
     369              : 
     370            0 : void MsProcess::startWithArgs(const QString& program, const QStringList& args)
     371              : {
     372            0 :     QProcess::start(program, args, ReadWrite);
     373            0 : }
     374              : }
        

Generated by: LCOV version 2.0-1