Revision control

++ // Copyright (C) 2018 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant
++
++ #include "qqmlpreviewfileengine.h"
++ #include "qqmlpreviewservice.h"
++
++ #include <QtCore/qlibraryinfo.h>
++ #include <QtCore/qthread.h>
++ #include <QtCore/qwaitcondition.h>
++
++ #include <cstring>
++
++ QT_BEGIN_NAMESPACE
++
++ static bool isRelative(const QString &path)
++ {
++ if (path.isEmpty())
++ return true;
++ if (path.at(0) == '/')
++ return false;
++ if (path.at(0) == ':' && path.size() >= 2 && path.at(1) == '/')
++ return false;
++ #ifdef Q_OS_WIN
++ if (path.length() >= 2 && path.at(1) == ':')
++ return false;
++ #endif
++ return true;
++ }
++
++ static QString absolutePath(const QString &path)
++ {
++ return QDir::cleanPath(isRelative(path) ? (QDir::currentPath() + '/' + path) : path);
++ }
++
++ bool isRootPath(const QString &path)
++ {
++ return QFileSystemEntry::isRootPath(path);
++ }
++
++ class QQmlPreviewFileEngineIterator : public QAbstractFileEngineIterator
++ {
++ public:
++ QQmlPreviewFileEngineIterator(const QString &path, QDirListing::IteratorFlags filters,
++ const QStringList &filterNames, const QStringList &m_entries);
++ ~QQmlPreviewFileEngineIterator();
++
++ bool advance() override;
++ QString currentFileName() const override;
++
++ private:
++ const QStringList m_entries;
++ int m_index;
++ };
++
++ QQmlPreviewFileEngineIterator::QQmlPreviewFileEngineIterator(const QString &path,
++ QDirListing::IteratorFlags filters,
++ const QStringList &filterNames,
++ const QStringList &entries)
++ : QAbstractFileEngineIterator(path, filters, filterNames), m_entries(entries), m_index(0)
++ {
++ }
++
++ QQmlPreviewFileEngineIterator::~QQmlPreviewFileEngineIterator()
++ {
++ }
++
++ bool QQmlPreviewFileEngineIterator::advance()
++ {
++ if (m_index >= m_entries.size())
++ return false;
++
++ ++m_index;
++ return true;
++ }
++
++ QString QQmlPreviewFileEngineIterator::currentFileName() const
++ {
++ if (m_index == 0 || m_index > m_entries.size())
++ return QString();
++ return m_entries.at(m_index - 1);
++ }
++
++ QQmlPreviewFileEngine::QQmlPreviewFileEngine(const QString &file, const QString &absolute,
++ QQmlPreviewFileLoader *loader) :
++ m_name(file), m_absolute(absolute), m_loader(loader)
++ {
++ load();
++ }
++
++ void QQmlPreviewFileEngine::setFileName(const QString &file)
++ {
++ m_name = file;
++ m_absolute = absolutePath(file);
++ m_fallback.reset();
++ m_contents.close();
++ m_contents.setData(QByteArray());
++ m_entries.clear();
++ load();
++ }
++
++ bool QQmlPreviewFileEngine::open(QIODevice::OpenMode flags,
++ std::optional<QFile::Permissions> permissions)
++ {
++ switch (m_result) {
++ case QQmlPreviewFileLoader::File:
++ return m_contents.open(flags);
++ case QQmlPreviewFileLoader::Directory:
++ return false;
++ case QQmlPreviewFileLoader::Fallback:
++ return m_fallback->open(flags, permissions);
++ default:
++ Q_UNREACHABLE_RETURN(false);
++ }
++ }
++
++ bool QQmlPreviewFileEngine::close()
++ {
++ switch (m_result) {
++ case QQmlPreviewFileLoader::Fallback:
++ return m_fallback->close();
++ case QQmlPreviewFileLoader::File:
++ m_contents.close();
++ return true;
++ case QQmlPreviewFileLoader::Directory:
++ return false;
++ default:
++ Q_UNREACHABLE_RETURN(false);
++ }
++ }
++
++ qint64 QQmlPreviewFileEngine::size() const
++ {
++ return m_fallback ? m_fallback->size() : m_contents.size();
++ }
++
++ qint64 QQmlPreviewFileEngine::pos() const
++ {
++ return m_fallback ? m_fallback->pos() : m_contents.pos();
++ }
++
++ bool QQmlPreviewFileEngine::seek(qint64 newPos)
++ {
++ return m_fallback? m_fallback->seek(newPos) : m_contents.seek(newPos);
++ }
++
++ qint64 QQmlPreviewFileEngine::read(char *data, qint64 maxlen)
++ {
++ return m_fallback ? m_fallback->read(data, maxlen) : m_contents.read(data, maxlen);
++ }
++
++ QAbstractFileEngine::FileFlags QQmlPreviewFileEngine::fileFlags(
++ QAbstractFileEngine::FileFlags type) const
++ {
++ if (m_fallback)
++ return m_fallback->fileFlags(type);
++
++ QAbstractFileEngine::FileFlags ret;
++
++ if (type & PermsMask) {
++ ret |= QAbstractFileEngine::FileFlags(
++ ReadOwnerPerm | ReadUserPerm | ReadGroupPerm | ReadOtherPerm);
++ }
++
++ if (type & TypesMask) {
++ if (m_result == QQmlPreviewFileLoader::Directory)
++ ret |= DirectoryType;
++ else
++ ret |= FileType;
++ }
++
++ if (type & FlagsMask) {
++ ret |= ExistsFlag;
++ if (isRootPath(m_name))
++ ret |= RootFlag;
++ }
++
++ return ret;
++ }
++
++ QString QQmlPreviewFileEngine::fileName(QAbstractFileEngine::FileName file) const
++ {
++ if (m_fallback)
++ return m_fallback->fileName(file);
++
++ if (file == BaseName) {
++ int slashPos = m_name.lastIndexOf('/');
++ if (slashPos == -1)
++ return m_name;
++ return m_name.mid(slashPos + 1);
++ } else if (file == PathName || file == AbsolutePathName) {
++ const QString path = (file == AbsolutePathName) ? m_absolute : m_name;
++ const int slashPos = path.lastIndexOf('/');
++ if (slashPos == -1)
++ return QString();
++ else if (slashPos == 0)
++ return "/";
++ return path.left(slashPos);
++ } else if (file == CanonicalName || file == CanonicalPathName) {
++ if (file == CanonicalPathName) {
++ const int slashPos = m_absolute.lastIndexOf('/');
++ if (slashPos != -1)
++ return m_absolute.left(slashPos);
++ }
++ return m_absolute;
++ }
++ return m_name;
++ }
++
++ uint QQmlPreviewFileEngine::ownerId(QAbstractFileEngine::FileOwner owner) const
++ {
++ return m_fallback ? m_fallback->ownerId(owner) : static_cast<uint>(-2);
++ }
++
++ QAbstractFileEngine::IteratorUniquePtr QQmlPreviewFileEngine::beginEntryList(
++ const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames)
++ {
++ return m_fallback ? m_fallback->beginEntryList(path, filters, filterNames)
++ : std::make_unique<QQmlPreviewFileEngineIterator>(
++ path, filters, filterNames, m_entries);
++ }
++
++ QAbstractFileEngine::IteratorUniquePtr QQmlPreviewFileEngine::endEntryList()
++ {
++ return m_fallback ? m_fallback->endEntryList() : nullptr;
++ }
++
++ bool QQmlPreviewFileEngine::flush()
++ {
++ return m_fallback ? m_fallback->flush() : true;
++ }
++
++ bool QQmlPreviewFileEngine::syncToDisk()
++ {
++ return m_fallback ? m_fallback->syncToDisk() : false;
++ }
++
++ bool QQmlPreviewFileEngine::isSequential() const
++ {
++ return m_fallback ? m_fallback->isSequential() : m_contents.isSequential();
++ }
++
++ bool QQmlPreviewFileEngine::remove()
++ {
++ return m_fallback ? m_fallback->remove() : false;
++ }
++
++ bool QQmlPreviewFileEngine::copy(const QString &newName)
++ {
++ return m_fallback ? m_fallback->copy(newName) : false;
++ }
++
++ bool QQmlPreviewFileEngine::rename(const QString &newName)
++ {
++ return m_fallback ? m_fallback->rename(newName) : false;
++ }
++
++ bool QQmlPreviewFileEngine::renameOverwrite(const QString &newName)
++ {
++ return m_fallback ? m_fallback->renameOverwrite(newName) : false;
++ }
++
++ bool QQmlPreviewFileEngine::link(const QString &newName)
++ {
++ return m_fallback ? m_fallback->link(newName) : false;
++ }
++
++ bool QQmlPreviewFileEngine::mkdir(const QString &dirName, bool createParentDirectories,
++ std::optional<QFile::Permissions> permissions) const
++ {
++ if (m_fallback)
++ return m_fallback->mkdir(dirName, createParentDirectories, permissions);
++
++ if (isRelative(dirName))
++ return false;
++
++ // If we're creating a directory from an absolute path, it can be anywhere.
++ // The result is unrelated to the currently running preview.
++ return QAbstractFileEngine::create(QDir::rootPath())
++ ->mkdir(dirName, createParentDirectories, permissions);
++ }
++
++ bool QQmlPreviewFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const
++ {
++ if (m_fallback)
++ m_fallback->rmdir(dirName, recurseParentDirectories);
++
++ if (isRelative(dirName))
++ return false;
++
++ // If we're removing a directory at an absolute path, it can be anywhere.
++ // The result is unrelated to the currently running preview.
++ return QAbstractFileEngine::create(QDir::rootPath())
++ ->rmdir(dirName, recurseParentDirectories);
++ }
++
++ bool QQmlPreviewFileEngine::setSize(qint64 size)
++ {
++ switch (m_result) {
++ case QQmlPreviewFileLoader::Fallback:
++ return m_fallback->setSize(size);
++ case QQmlPreviewFileLoader::File:
++ if (size < 0 || size > std::numeric_limits<int>::max())
++ return false;
++ m_contents.buffer().resize(static_cast<int>(size));
++ return true;
++ case QQmlPreviewFileLoader::Directory:
++ return false;
++ default:
++ Q_UNREACHABLE_RETURN(false);
++ }
++ }
++
++ bool QQmlPreviewFileEngine::caseSensitive() const
++ {
++ return m_fallback ? m_fallback->caseSensitive() : true;
++ }
++
++ bool QQmlPreviewFileEngine::isRelativePath() const
++ {
++ return m_fallback ? m_fallback->isRelativePath() : isRelative(m_name);
++ }
++
++ bool QQmlPreviewFileEngine::setPermissions(uint perms)
++ {
++ return m_fallback ? m_fallback->setPermissions(perms) : false;
++ }
++
++ QByteArray QQmlPreviewFileEngine::id() const
++ {
++ return m_fallback ? m_fallback->id() : QByteArray();
++ }
++
++ QString QQmlPreviewFileEngine::owner(FileOwner owner) const
++ {
++ return m_fallback ? m_fallback->owner(owner) : QString();
++ }
++
++ QDateTime QQmlPreviewFileEngine::fileTime(QFile::FileTime time) const
++ {
++ // Files we replace are always newer than the ones we had before. This makes the QML engine
++ // actually recompile them, rather than pick them from the cache.
++ return m_fallback ? m_fallback->fileTime(time) : QDateTime::currentDateTime();
++ }
++
++ int QQmlPreviewFileEngine::handle() const
++ {
++ return m_fallback ? m_fallback->handle() : -1;
++ }
++
++ qint64 QQmlPreviewFileEngine::readLine(char *data, qint64 maxlen)
++ {
++ return m_fallback ? m_fallback->readLine(data, maxlen) : m_contents.readLine(data, maxlen);
++ }
++
++ qint64 QQmlPreviewFileEngine::write(const char *data, qint64 len)
++ {
++ return m_fallback ? m_fallback->write(data, len) : m_contents.write(data, len);
++ }
++
++ bool QQmlPreviewFileEngine::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output)
++ {
++ return m_fallback ? m_fallback->extension(extension, option, output) : false;
++ }
++
++ bool QQmlPreviewFileEngine::supportsExtension(Extension extension) const
++ {
++ return m_fallback ? m_fallback->supportsExtension(extension) : false;
++ }
++
++ void QQmlPreviewFileEngine::load() const
++ {
++ // We can get here from different threads on different instances of QQmlPreviewFileEngine.
++ // However, there is only one loader per QQmlPreviewFileEngineHandler and it is not thread-safe.
++ // Its content mutex doesn't help us here because we explicitly wait on it in load(), which
++ // causes it to be released. Therefore, lock the load mutex first.
++ // This doesn't cause any deadlocks because the only thread that wakes the loader on the content
++ // mutex never calls load(). It's the QML debug server thread that handles the debug protocol.
++ QMutexLocker loadLocker(m_loader->loadMutex());
++
++ m_result = m_loader->load(m_absolute);
++ switch (m_result) {
++ case QQmlPreviewFileLoader::File:
++ m_contents.setData(m_loader->contents());
++ break;
++ case QQmlPreviewFileLoader::Directory:
++ m_entries = m_loader->entries();
++ break;
++ case QQmlPreviewFileLoader::Fallback:
++ m_fallback = QAbstractFileEngine::create(m_name);
++ break;
++ case QQmlPreviewFileLoader::Unknown:
++ Q_UNREACHABLE();
++ break;
++ }
++ }
++
++ QQmlPreviewFileEngineHandler::QQmlPreviewFileEngineHandler(QQmlPreviewFileLoader *loader)
++ : m_loader(loader)
++ {
++ }
++
++ std::unique_ptr<QAbstractFileEngine> QQmlPreviewFileEngineHandler::create(
++ const QString &fileName) const
++ {
++ using namespace Qt::StringLiterals;
++ static QList<QLatin1StringView> prohibitedSuffixes {
++ // Don't load compiled QML/JS over the network
++ ".qmlc"_L1, ".jsc"_L1, ".mjsc"_L1,
++
++ // Don't load plugins over the network
++ ".dll"_L1, ".so"_L1, ".dylib"_L1
++ };
++
++ for (QLatin1StringView suffix : prohibitedSuffixes) {
++ if (fileName.endsWith(suffix))
++ return nullptr;
++ }
++
++ if (isRootPath(fileName))
++ return nullptr;
++
++ QString relative = fileName;
++ while (relative.endsWith('/'))
++ relative.chop(1);
++
++ if (relative.isEmpty() || relative == ":")
++ return nullptr;
++
++ const QString absolute = relative.startsWith(':') ? relative : absolutePath(relative);
++
++ if (m_loader->isBlacklisted(absolute))
++ return {};
++
++ return std::make_unique<QQmlPreviewFileEngine>(relative, absolute, m_loader.data());
++ }
++
++ QT_END_NAMESPACE