diff options
Diffstat (limited to 'src')
31 files changed, 987 insertions, 176 deletions
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index f31968f8199..32b70a1f288 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -701,6 +701,7 @@ qt_internal_extend_target(Core CONDITION APPLE kernel/qcore_mac.mm kernel/qcore_mac_p.h kernel/qcoreapplication_mac.cpp kernel/qeventdispatcher_cf.mm kernel/qeventdispatcher_cf_p.h + platform/darwin/qdarwinsecurityscopedfileengine.mm platform/darwin/qdarwinsecurityscopedfileengine_p.h LIBRARIES ${FWCoreFoundation} ${FWFoundation} diff --git a/src/corelib/doc/qtcore.qdocconf b/src/corelib/doc/qtcore.qdocconf index d2b386373a0..b3e4e9d30a9 100644 --- a/src/corelib/doc/qtcore.qdocconf +++ b/src/corelib/doc/qtcore.qdocconf @@ -21,7 +21,7 @@ qhp.QtCore.virtualFolder = qtcore qhp.QtCore.indexTitle = Qt Core qhp.QtCore.indexRoot = -qhp.QtCore.subprojects = manual classes +qhp.QtCore.subprojects = manual examples classes qhp.QtCore.subprojects.manual.title = Qt Core qhp.QtCore.subprojects.manual.indexTitle = Qt Core module topics qhp.QtCore.subprojects.manual.type = manual @@ -31,6 +31,11 @@ qhp.QtCore.subprojects.classes.indexTitle = Qt Core C++ Classes qhp.QtCore.subprojects.classes.selectors = class fake:headerfile qhp.QtCore.subprojects.classes.sortPages = true +qhp.QtCore.subprojects.examples.title = Examples +qhp.QtCore.subprojects.examples.indexTitle = Qt Core Examples +qhp.QtCore.subprojects.examples.selectors = example +qhp.QtCore.subprojects.examples.sortPages = true + tagfile = ../../../doc/qtcore/qtcore.tags # Make QtCore depend on all doc modules; this ensures complete inheritance diff --git a/src/corelib/doc/src/qtcore.qdoc b/src/corelib/doc/src/qtcore.qdoc index ec5fa564639..fbcd02aeea5 100644 --- a/src/corelib/doc/src/qtcore.qdoc +++ b/src/corelib/doc/src/qtcore.qdoc @@ -31,3 +31,12 @@ target_link_libraries(mytarget PRIVATE Qt6::CorePrivate) \endcode */ + +/*! + \group corelib_examples + \title Qt Core Examples + + \brief Examples for the Qt Core. + + To learn how to use features of the Qt Core module, see examples: +*/ diff --git a/src/corelib/io/qfile.cpp b/src/corelib/io/qfile.cpp index e1fc043a0ff..0184fd838aa 100644 --- a/src/corelib/io/qfile.cpp +++ b/src/corelib/io/qfile.cpp @@ -592,6 +592,10 @@ QFile::rename(const QString &newName) return false; } + // Keep engine for target alive during the operation + // FIXME: Involve the target engine in the operation + auto targetEngine = QFileSystemEngine::createLegacyEngine(newName); + // If the file exists and it is a case-changing rename ("foo" -> "Foo"), // compare Ids to make sure it really is a different file. // Note: this does not take file engines into account. @@ -738,6 +742,11 @@ QFile::link(const QString &linkName) qWarning("QFile::link: Empty or null file name"); return false; } + + // Keep engine for target alive during the operation + // FIXME: Involve the target engine in the operation + auto targetEngine = QFileSystemEngine::createLegacyEngine(linkName); + QFileInfo fi(linkName); if (d->engine()->link(fi.absoluteFilePath())) { unsetError(); @@ -771,6 +780,10 @@ bool QFilePrivate::copy(const QString &newName) Q_ASSERT(error == QFile::NoError); Q_ASSERT(!q->isOpen()); + // Keep engine for target alive during the operation + // FIXME: Involve the target engine in the operation + auto targetEngine = QFileSystemEngine::createLegacyEngine(newName); + // Some file engines can perform this copy more efficiently (e.g., Windows // calling CopyFile). if (engine()->copy(newName)) diff --git a/src/corelib/io/qfilesystemengine.cpp b/src/corelib/io/qfilesystemengine.cpp index 03da2331e05..46d4cb709e2 100644 --- a/src/corelib/io/qfilesystemengine.cpp +++ b/src/corelib/io/qfilesystemengine.cpp @@ -190,6 +190,14 @@ QFileSystemEngine::createLegacyEngine(QFileSystemEntry &entry, QFileSystemMetaDa return engine; } +std::unique_ptr<QAbstractFileEngine> +QFileSystemEngine::createLegacyEngine(const QString &fileName) +{ + QFileSystemEntry entry(fileName); + QFileSystemMetaData metaData; + return createLegacyEngine(entry, metaData); +} + //static QString QFileSystemEngine::resolveUserName(const QFileSystemEntry &entry, QFileSystemMetaData &metaData) { diff --git a/src/corelib/io/qfilesystemengine_p.h b/src/corelib/io/qfilesystemengine_p.h index ee70ccc1e1b..46eeeda569e 100644 --- a/src/corelib/io/qfilesystemengine_p.h +++ b/src/corelib/io/qfilesystemengine_p.h @@ -161,6 +161,8 @@ public: static std::unique_ptr<QAbstractFileEngine> createLegacyEngine(QFileSystemEntry &entry, QFileSystemMetaData &data); + static std::unique_ptr<QAbstractFileEngine> + createLegacyEngine(const QString &fileName); private: static QString slowCanonicalized(const QString &path); diff --git a/src/corelib/io/qfsfileengine_p.h b/src/corelib/io/qfsfileengine_p.h index 2de6cb0cb73..8ad673bf0bf 100644 --- a/src/corelib/io/qfsfileengine_p.h +++ b/src/corelib/io/qfsfileengine_p.h @@ -82,7 +82,7 @@ public: bool setFileTime(const QDateTime &newDate, QFile::FileTime time) override; QDateTime fileTime(QFile::FileTime time) const override; void setFileName(const QString &file) override; - void setFileEntry(QFileSystemEntry &&entry); + virtual void setFileEntry(QFileSystemEntry &&entry); int handle() const override; #ifndef QT_NO_FILESYSTEMITERATOR diff --git a/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm new file mode 100644 index 00000000000..cb38445f4fe --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm @@ -0,0 +1,552 @@ +// Copyright (C) 2025 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 reason:default + +#include "qdarwinsecurityscopedfileengine_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qstandardpaths.h> +#include <QtCore/qreadwritelock.h> +#include <QtCore/qscopedvaluerollback.h> + +#include <QtCore/private/qcore_mac_p.h> +#include <QtCore/private/qfsfileengine_p.h> +#include <QtCore/private/qfilesystemengine_p.h> + +#include <thread> +#include <mutex> + +#include <CoreFoundation/CoreFoundation.h> +#include <Foundation/NSURL.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Q_STATIC_LOGGING_CATEGORY(lcSecEngine, "qt.core.io.security-scoped-fileengine", QtCriticalMsg) + +template<typename T> class BackgroundLoader; + +/* + File engine handler for security scoped file paths. + + Installs itself as soon as QtCore is loaded if the application + is sandboxed (optionally on macOS, and always on iOS and friends). +*/ +class SecurityScopedFileEngineHandler : public QAbstractFileEngineHandler +{ +public: + SecurityScopedFileEngineHandler(); + ~SecurityScopedFileEngineHandler(); + + void registerPossiblySecurityScopedURL(NSURL *url); + + std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override; + + static BackgroundLoader<SecurityScopedFileEngineHandler>& get(); + +private: + Q_DISABLE_COPY_MOVE(SecurityScopedFileEngineHandler) + + void saveBookmark(NSURL *url); + void saveBookmarks(); + + NSURL *bookmarksFile() const; + + static NSString *cacheKeyForUrl(NSURL *url); + static NSString *cacheKeyForPath(const QString &url); + + NSMutableDictionary *m_bookmarks = nullptr; + mutable QReadWriteLock m_bookmarkLock; + + friend class SecurityScopedFileEngine; +}; + +/* + Helper class for asynchronous instantiation of types. +*/ +template<typename T> +class BackgroundLoader +{ +public: + explicit BackgroundLoader(bool shouldLoad) { + if (shouldLoad) { + m_thread = std::thread([this]() { + m_instance = std::make_unique<T>(); + }); + } + } + + ~BackgroundLoader() + { + std::scoped_lock lock(m_mutex); + if (m_thread.joinable()) + m_thread.join(); + } + + T* operator->() const + { + std::scoped_lock lock(m_mutex); + if (m_thread.joinable()) + m_thread.join(); + return m_instance.get(); + } + + explicit operator bool() const + { + std::scoped_lock lock(m_mutex); + return m_thread.joinable() || m_instance; + } + +private: + mutable std::mutex m_mutex; + mutable std::thread m_thread; + std::unique_ptr<T> m_instance; +}; + +/* + Thread-safe background-loading of optional security scoped handler, + with the ability to kick off instantiation early during program load. +*/ +BackgroundLoader<SecurityScopedFileEngineHandler>& SecurityScopedFileEngineHandler::get() +{ + using Handler = BackgroundLoader<SecurityScopedFileEngineHandler>; + static Handler handler = []() -> Handler { + if (!qt_apple_isSandboxed()) + return Handler{false}; + + qCInfo(lcSecEngine) << "Application sandbox is active. Registering security-scoped file engine."; + return Handler{true}; + }(); + return handler; +} + +static void initializeSecurityScopedFileEngineHandler() +{ + // Kick off loading of bookmarks early in the background + std::ignore = SecurityScopedFileEngineHandler::get(); +} +Q_CONSTRUCTOR_FUNCTION(initializeSecurityScopedFileEngineHandler); + +/* + Registration function for possibly security scoped URLs. + + Entry points that might provide security scoped URLs such as file + dialogs or drag-and-drop should use this function to ensure that + the security scoped file engine handler knows about the URL. +*/ +QUrl qt_apple_urlFromPossiblySecurityScopedURL(NSURL *url) +{ + if (auto &handler = SecurityScopedFileEngineHandler::get()) + handler->registerPossiblySecurityScopedURL(url); + + // Note: The URL itself doesn't encode any of the bookmark data, + // neither in the scheme or as fragments or query parameters, + // as it's all handled by the bookmark cache in the file engine. + return QUrl(QString::fromNSString(url.absoluteString) + .normalized(QString::NormalizationForm_C)); +} + +static bool checkIfResourceIsReachable(NSURL *url) +{ + NSError *error = nullptr; + if ([url checkResourceIsReachableAndReturnError:&error]) + return true; + + // Our goal is to check whether the file exists or not, and if + // not, defer creating a bookmark for it. If we get any other + // error we want to know. + if (![error.domain isEqualToString:NSCocoaErrorDomain] || error.code != NSFileReadNoSuchFileError) { + qCWarning(lcSecEngine) << "Unexpected" << error + << "when resolving reachability for" << url; + } + + return false; +} + +/* + File engine for maintaining access lifetime of security-scoped + resources on sandboxed Apple platforms. + + Note that there isn't necessarily a 1:1 relationship between + the file being operated on by the QFSFileEngine and the security + scoped resource that allows access to it, for example in the + case of a folder giving access to all files (and sub-folders) + within it. +*/ +class SecurityScopedFileEngine : public QFSFileEngine +{ + Q_DECLARE_PRIVATE(QFSFileEngine) +public: + SecurityScopedFileEngine(const QString &fileName, NSURL *securityScopedUrl) + : QFSFileEngine(fileName) + , m_securityScopedUrl([securityScopedUrl retain]) + { + startAccessingSecurityScopedResource(); + } + + ~SecurityScopedFileEngine() + { + stopAccessingSecurityScopedResource(); + [m_securityScopedUrl release]; + } + + void setFileName(const QString &fileName) override + { + QFileSystemEntry entry(fileName); + setFileEntry(std::move(entry)); + } + + void setFileEntry(QFileSystemEntry &&entry) override + { + // We can't rely on the new entry being accessible under the same + // security scope as the original path, or even that the new path + // is a security scoped resource, so stop access here, and start + // access for the new resource below if needed. + stopAccessingSecurityScopedResource(); + [m_securityScopedUrl release]; + m_securityScopedUrl = nil; + + const QString fileName = entry.filePath(); + QFSFileEngine::setFileEntry(std::move(entry)); + + // The new path may not be a security scoped resource, but if it is + // we need to establish access to it. The only way to do that is to + // actually create an engine for it, including resolving bookmarks. + auto newEngine = SecurityScopedFileEngineHandler::get()->create(fileName); + if (auto *engine = dynamic_cast<SecurityScopedFileEngine*>(newEngine.get())) { + m_securityScopedUrl = [engine->m_securityScopedUrl retain]; + startAccessingSecurityScopedResource(); + } + } + +private: + void startAccessingSecurityScopedResource() + { + if ([m_securityScopedUrl startAccessingSecurityScopedResource]) { + qCDebug(lcSecEngine) << "Started accessing" << m_securityScopedUrl.path + << "on behalf of" << fileName(DefaultName); + + m_securityScopeWasReachable = securityScopeIsReachable(); + } else { + qCWarning(lcSecEngine) << "Unexpectedly using security scoped" + << "file engine for" << m_securityScopedUrl.path + << "on behalf of" << fileName(DefaultName) + << "without needing scoped access"; + } + } + + void stopAccessingSecurityScopedResource() + { + if (!m_securityScopeWasReachable && securityScopeIsReachable()) { + // The security scoped URL didn't exist when we first started + // accessing it, but it does now, so persist a bookmark for it. + qCDebug(lcSecEngine) << "Security scoped resource has been created. Saving bookmark."; + SecurityScopedFileEngineHandler::get()->saveBookmark(m_securityScopedUrl); + } + + // Note: Stopping access is a no-op if we didn't have access + [m_securityScopedUrl stopAccessingSecurityScopedResource]; + qCDebug(lcSecEngine) << "Stopped accessing" << m_securityScopedUrl.path + << "on behalf of" << fileName(DefaultName); + } + + bool securityScopeIsReachable() const + { + return checkIfResourceIsReachable(m_securityScopedUrl); + } + + // See note above about relationship to fileName + NSURL *m_securityScopedUrl = nullptr; + bool m_securityScopeWasReachable = false; +}; + +// ---------------------------------------------------------------------- + +SecurityScopedFileEngineHandler::SecurityScopedFileEngineHandler() +{ + QMacAutoReleasePool pool; + + NSURL *savedBookmarks = bookmarksFile(); + if ([NSFileManager.defaultManager fileExistsAtPath:savedBookmarks.path]) { + NSError *error = nullptr; + m_bookmarks = [[NSDictionary dictionaryWithContentsOfURL:savedBookmarks + error:&error] mutableCopy]; + + if (error) { + qCWarning(lcSecEngine) << "Failed to load bookmarks from" + << savedBookmarks << ":" << error; + } else { + qCInfo(lcSecEngine) << "Loaded existing bookmarks for" << m_bookmarks.allKeys; + } + } + + if (!m_bookmarks) + m_bookmarks = [NSMutableDictionary new]; +} + +SecurityScopedFileEngineHandler::~SecurityScopedFileEngineHandler() +{ + [m_bookmarks release]; +} + +void SecurityScopedFileEngineHandler::registerPossiblySecurityScopedURL(NSURL *url) +{ + QMacAutoReleasePool pool; + + // Start accessing the resource, to check if it's security scoped, + // and allow us to create a bookmark for it on both macOS and iOS. + if (![url startAccessingSecurityScopedResource]) + return; // All good, not security scoped + + if (checkIfResourceIsReachable(url)) { + // We can access the resource, which means it exists, so we can + // create a persistent bookmark for it right away. We want to do + // this as soon as possible, so that if the app is terminated the + // user can continue working on the file without the app needing + // to ask for access again via a file dialog. + saveBookmark(url); + } else { + // The file isn't accessible, likely because it doesn't exist. + // As we can only create security scoped bookmarks for files + // that exist we store the URL itself for now, and save it to + // a bookmark later when we detect that the file has been created. + qCInfo(lcSecEngine) << "Resource is not reachable." + << "Registering URL" << url << "instead"; + QWriteLocker locker(&m_bookmarkLock); + m_bookmarks[cacheKeyForUrl(url)] = url; + } + + // Balance access from above + [url stopAccessingSecurityScopedResource]; + +#if defined(Q_OS_MACOS) + // On macOS, unlike iOS, URLs from file dialogs, etc, come with implicit + // access already, and we are expected to balance this access with an + // explicit stopAccessingSecurityScopedResource. We release the last + // access here to unify the behavior between macOS and iOS, and then + // leave it up to the SecurityScopedFileEngine to regain access, where + // we know the lifetime of resource use, and when to release access. + [url stopAccessingSecurityScopedResource]; +#endif +} + +std::unique_ptr<QAbstractFileEngine> SecurityScopedFileEngineHandler::create(const QString &fileName) const +{ + QMacAutoReleasePool pool; + + static thread_local bool recursionGuard = false; + if (recursionGuard) + return nullptr; + + if (fileName.isEmpty()) + return nullptr; + + QFileSystemEntry fileSystemEntry(fileName); + QFileSystemMetaData metaData; + + { + // Check if there's another engine that claims to handle the given file name. + // This covers non-QFSFileEngines like QTemporaryFileEngine, and QResourceFileEngine. + // If there isn't one, we'll get nullptr back, and know that we can access the + // file via our special QFSFileEngine. + QScopedValueRollback<bool> rollback(recursionGuard, true); + if (auto engine = QFileSystemEngine::createLegacyEngine(fileSystemEntry, metaData)) { + // Shortcut the logic of the createLegacyEngine call we're in by + // just returning this engine now. + qCDebug(lcSecEngine) << "Preferring non-QFSFileEngine engine" + << engine.get() << "for" << fileName; + return engine; + } + } + + // We're mapping the file name to existing bookmarks below, so make sure + // we use as close as we can get to the canonical path. For files that + // do not exist we fall back to the cleaned absolute path. + auto canonicalEntry = QFileSystemEngine::canonicalName(fileSystemEntry, metaData); + if (canonicalEntry.isEmpty()) + canonicalEntry = QFileSystemEngine::absoluteName(fileSystemEntry); + + if (canonicalEntry.isRelative()) { + // We try to map relative paths to absolute above, but doing so requires + // knowledge of the current working directory, which we only have if the + // working directory has already started access through other means. We + // can't explicitly start access of the working directory here, as doing + // so requires its name, which we can't get from getcwd() without access. + // Fortunately all of the entry points of security scoped URLs such as + // file dialogs or drag-and-drop give us absolute paths, and APIs like + // QDir::filePath() will construct absolute URLs without needing the + // current working directory. + qCWarning(lcSecEngine) << "Could not resolve" << fileSystemEntry.filePath() + << "against current working working directory"; + return nullptr; + } + + // Clean the path as well, to remove any trailing slashes for directories + QString filePath = QDir::cleanPath(canonicalEntry.filePath()); + + // Files inside the sandbox container can always be accessed directly + static const QString sandboxRoot = QString::fromNSString(NSHomeDirectory()); + if (filePath.startsWith(sandboxRoot)) + return nullptr; + + // The same applies to files inside the application's own bundle + static const QString bundleRoot = QString::fromNSString(NSBundle.mainBundle.bundlePath); + if (filePath.startsWith(bundleRoot)) + return nullptr; + + qCDebug(lcSecEngine) << "Looking up bookmark for" << filePath << "based on incoming fileName" << fileName; + + // Check if we have a persisted bookmark for this fileName, or + // any of its containing directories (which will give us access + // to the file). + QReadLocker locker(&m_bookmarkLock); + auto *cacheKey = cacheKeyForPath(filePath); + NSObject *bookmarkData = nullptr; + while (cacheKey.length > 1) { + bookmarkData = m_bookmarks[cacheKey]; + if (bookmarkData) + break; + cacheKey = [cacheKey stringByDeletingLastPathComponent]; + } + + // We didn't find a bookmark, so there's no point in trying to manage + // this file via a SecurityScopedFileEngine. + if (!bookmarkData) { + qCDebug(lcSecEngine) << "No bookmark found. Falling back to QFSFileEngine."; + return nullptr; + } + + NSURL *securityScopedUrl = nullptr; + if ([bookmarkData isKindOfClass:NSURL.class]) { + securityScopedUrl = static_cast<NSURL*>(bookmarkData); + } else { + NSError *error = nullptr; + BOOL bookmarkDataIsStale = NO; + securityScopedUrl = [NSURL URLByResolvingBookmarkData:static_cast<NSData*>(bookmarkData) + options: + #if defined(Q_OS_MACOS) + NSURLBookmarkResolutionWithSecurityScope + #else + // iOS bookmarks are always security scoped, and we + // don't need or want any of the other options. + NSURLBookmarkResolutionOptions(0) + #endif + relativeToURL:nil /* app-scoped bookmark */ + bookmarkDataIsStale:&bookmarkDataIsStale + error:&error]; + + if (!securityScopedUrl || error) { + qCWarning(lcSecEngine) << "Failed to resolve bookmark data for" + << fileName << ":" << error; + return nullptr; + } + + if (bookmarkDataIsStale) { + // This occurs when for example the file has been renamed, moved, + // or deleted. Normally this would be the place to update the + // bookmark to point to the new location, but Qt clients may not + // be prepared for QFiles changing their file-names under their + // feet so we treat it as a missing file. + qCDebug(lcSecEngine) << "Bookmark for" << cacheKey << "was stale"; + locker.unlock(); + QWriteLocker writeLocker(&m_bookmarkLock); + [m_bookmarks removeObjectForKey:cacheKey]; + auto *mutableThis = const_cast<SecurityScopedFileEngineHandler*>(this); + mutableThis->saveBookmarks(); + return nullptr; + } + } + + qCInfo(lcSecEngine) << "Resolved security scope" << securityScopedUrl + << "for path" << filePath; + return std::make_unique<SecurityScopedFileEngine>(fileName, securityScopedUrl); +} + +/* + Create an app-scoped bookmark, and store it in our persistent cache. + + We do this so that the user can continue accessing the file even after + application restarts. + + Storing the bookmarks to disk (inside the sandbox) is safe, as only the + app that created the app-scoped bookmarks can obtain access to the file + system resource that the URL points to. Specifically, a bookmark created + with security scope fails to resolve if the caller does not have the same + code signing identity as the caller that created the bookmark. +*/ +void SecurityScopedFileEngineHandler::saveBookmark(NSURL *url) +{ + NSError *error = nullptr; + NSData *bookmarkData = [url bookmarkDataWithOptions: + #if defined(Q_OS_MACOS) + NSURLBookmarkCreationWithSecurityScope + #else + // iOS bookmarks are always security scoped, and we + // don't need or want any of the other options. + NSURLBookmarkCreationOptions(0) + #endif + includingResourceValuesForKeys:nil + relativeToURL:nil /* app-scoped bookmark */ + error:&error]; + + if (bookmarkData) { + QWriteLocker locker(&m_bookmarkLock); + NSString *cacheKey = cacheKeyForUrl(url); + qCInfo(lcSecEngine) + << (m_bookmarks[cacheKey] ? "Updating" : "Registering") + << "bookmark for" << cacheKey; + m_bookmarks[cacheKey] = bookmarkData; + saveBookmarks(); + } else { + qCWarning(lcSecEngine) << "Failed to create bookmark data for" << url << error; + } +} + +/* + Saves the bookmarks cache to disk. + + We do this preemptively whenever we create a bookmark, to ensure + the file can be accessed later on even if the app crashes. +*/ +void SecurityScopedFileEngineHandler::saveBookmarks() +{ + QMacAutoReleasePool pool; + + NSError *error = nullptr; + NSURL *bookmarksFilePath = bookmarksFile(); + [NSFileManager.defaultManager + createDirectoryAtURL:[bookmarksFilePath URLByDeletingLastPathComponent] + withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { + qCWarning(lcSecEngine) << "Failed to create bookmarks path:" << error; + return; + } + [m_bookmarks writeToURL:bookmarksFile() error:&error]; + if (error) { + qCWarning(lcSecEngine) << "Failed to save bookmarks to" + << bookmarksFile() << ":" << error; + } +} + +NSURL *SecurityScopedFileEngineHandler::bookmarksFile() const +{ + NSURL *appSupportDir = [[NSFileManager.defaultManager URLsForDirectory: + NSApplicationSupportDirectory inDomains:NSUserDomainMask] firstObject]; + return [appSupportDir URLByAppendingPathComponent:@"SecurityScopedBookmarks.plist"]; +} + +NSString *SecurityScopedFileEngineHandler::cacheKeyForUrl(NSURL *url) +{ + return cacheKeyForPath(QString::fromNSString(url.path)); +} + +NSString *SecurityScopedFileEngineHandler::cacheKeyForPath(const QString &path) +{ + auto normalized = path.normalized(QString::NormalizationForm_D); + // We assume the file paths we get via file dialogs and similar + // are already canonical, but clean it just in case. + return QDir::cleanPath(normalized).toNSString(); +} + +QT_END_NAMESPACE diff --git a/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h new file mode 100644 index 00000000000..f6098fa977d --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h @@ -0,0 +1,29 @@ +// Copyright (C) 2025 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 reason:default + +#ifndef QDARWINSECURITYSCOPEDFILEENGINE_H +#define QDARWINSECURITYSCOPEDFILEENGINE_H + +// +// W A R N I N G +// ------------- +// +// This file is part of the QPA API and is not meant to be used +// in applications. Usage of this API may make your code +// source and binary incompatible with future versions of Qt. +// +// We mean it. +// + +#include <QtCore/qurl.h> + +Q_FORWARD_DECLARE_OBJC_CLASS(NSURL); + +QT_BEGIN_NAMESPACE + +Q_CORE_EXPORT QUrl qt_apple_urlFromPossiblySecurityScopedURL(NSURL *url); + +QT_END_NAMESPACE + +#endif // QDARWINSECURITYSCOPEDFILEENGINE_H diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index 287138bb915..4f3ecc4c6d9 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -178,12 +178,17 @@ Blob Blob::slice(uint32_t begin, uint32_t end) const ArrayBuffer Blob::arrayBuffer_sync() const { emscripten::val buffer; - uint32_t handlerIndex = qstdweb::Promise::make(m_blob, QStringLiteral("arrayBuffer"), { - .thenFunc = [&buffer](emscripten::val arrayBuffer) { - buffer = arrayBuffer; + QList<uint32_t> handlers; + qstdweb::Promise::make( + handlers, + m_blob, + QStringLiteral("arrayBuffer"), + { + .thenFunc = [&buffer](emscripten::val arrayBuffer) { + buffer = arrayBuffer; } }); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); return ArrayBuffer(buffer); } @@ -441,7 +446,7 @@ EventCallback::EventCallback(emscripten::val element, const std::string &name, } -uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks) +uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks, QList<uint32_t> *handlers) { Q_ASSERT_X(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc, "Promise::adoptPromise", "must provide at least one callback function"); @@ -498,14 +503,21 @@ uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbac promise = promise.call<emscripten::val>("finally", suspendResume->jsEventHandlerAt(*finallyIndex)); + if (handlers) { + if (thenIndex) + handlers->push_back(*thenIndex); + if (catchIndex) + handlers->push_back(*catchIndex); + handlers->push_back(*finallyIndex); + } return *finallyIndex; } -void Promise::suspendExclusive(uint32_t handlerIndex) +void Promise::suspendExclusive(QList<uint32_t> handlerIndices) { QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get(); Q_ASSERT(suspendResume); - suspendResume->suspendExclusive(handlerIndex); + suspendResume->suspendExclusive(handlerIndices); suspendResume->sendPendingEvents(); } @@ -657,11 +669,12 @@ void FileSystemWritableFileStreamIODevice::close() return; } - uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("close"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_stream.val(), QStringLiteral("close"), { .thenFunc = [](emscripten::val) { } }); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); QIODevice::close(); } @@ -683,14 +696,15 @@ bool FileSystemWritableFileStreamIODevice::seek(qint64 pos) emscripten::val seekParams = emscripten::val::object(); seekParams.set("type", std::string("seek")); seekParams.set("position", static_cast<double>(pos)); - uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("write"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_stream.val(), QStringLiteral("write"), { .thenFunc = [&success](emscripten::val) { success = true; }, .catchFunc = [](emscripten::val) { } }, seekParams); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); if (!success) return false; @@ -708,14 +722,15 @@ qint64 FileSystemWritableFileStreamIODevice::writeData(const char *data, qint64 bool success = false; Uint8Array array = Uint8Array::copyFrom(data, size); - uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("write"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_stream.val(), QStringLiteral("write"), { .thenFunc = [&success](emscripten::val) { success = true; }, .catchFunc = [](emscripten::val) { } }, array.val()); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); if (success) { qint64 newPos = pos() + size; @@ -770,7 +785,8 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) File file; bool success = false; - uint32_t handlerIndex = Promise::make(m_fileHandle.val(), QStringLiteral("getFile"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_fileHandle.val(), QStringLiteral("getFile"), { .thenFunc = [&file, &success](emscripten::val fileVal) { file = File(fileVal); success = true; @@ -778,7 +794,7 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) .catchFunc = [](emscripten::val) { } }); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); if (success) { m_blobDevice = std::make_unique<BlobIODevice>(file.slice(0, file.size())); @@ -796,7 +812,8 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) FileSystemWritableFileStream writableStream; bool success = false; - uint32_t handlerIndex = Promise::make(m_fileHandle.val(), QStringLiteral("createWritable"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_fileHandle.val(), QStringLiteral("createWritable"), { .thenFunc = [&writableStream, &success](emscripten::val writable) { writableStream = FileSystemWritableFileStream(writable); success = true; @@ -804,7 +821,7 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) .catchFunc = [](emscripten::val) { } }); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); if (success) { m_writableDevice = std::make_unique<FileSystemWritableFileStreamIODevice>(writableStream); diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h index 9a97370448e..b14d9e4012f 100644 --- a/src/corelib/platform/wasm/qstdweb_p.h +++ b/src/corelib/platform/wasm/qstdweb_p.h @@ -238,7 +238,7 @@ namespace qstdweb { }; namespace Promise { - uint32_t Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks); + uint32_t Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks, QList<uint32_t> *handlers = nullptr); template<typename... Args> uint32_t make(emscripten::val target, @@ -255,7 +255,24 @@ namespace qstdweb { return adoptPromise(std::move(promiseObject), std::move(callbacks)); } - void Q_CORE_EXPORT suspendExclusive(uint32_t handlerIndex); + template<typename... Args> + void make( + QList<uint32_t> &handlers, + emscripten::val target, + QString methodName, + PromiseCallbacks callbacks, + Args... args) + { + emscripten::val promiseObject = target.call<emscripten::val>( + methodName.toStdString().c_str(), std::forward<Args>(args)...); + if (promiseObject.isUndefined() || promiseObject["constructor"]["name"].as<std::string>() != "Promise") { + qFatal("This function did not return a promise"); + } + + adoptPromise(std::move(promiseObject), std::move(callbacks), &handlers); + } + + void Q_CORE_EXPORT suspendExclusive(QList<uint32_t> handlerIndices); void Q_CORE_EXPORT all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks); }; diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp index 5fe92926240..a4bc7843380 100644 --- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp +++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp @@ -196,9 +196,13 @@ void QWasmSuspendResumeControl::suspend() qtSuspendJs(); } -void QWasmSuspendResumeControl::suspendExclusive(uint32_t eventHandlerIndex) +void QWasmSuspendResumeControl::suspendExclusive(QList<uint32_t> eventHandlerIndices) { - suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndex); + m_eventFilter = [eventHandlerIndices](int handler) { + return eventHandlerIndices.contains(handler); + }; + + suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndices.back()); qtSuspendJs(); } @@ -211,37 +215,27 @@ int QWasmSuspendResumeControl::sendPendingEvents() emscripten::val control = suspendResumeControlJs(); emscripten::val pendingEvents = control["pendingEvents"]; - if (control["exclusiveEventHandler"].as<int>() > 0) - return sendPendingExclusiveEvent(); - - if (pendingEvents["length"].as<int>() == 0) - return 0; - int count = 0; - while (pendingEvents["length"].as<int>() > 0) { // Make sure it is reentrant - // Grab one event (handler and arg), and call it - emscripten::val event = pendingEvents.call<val>("shift"); - auto it = m_eventHandlers.find(event["index"].as<int>()); - if (it != m_eventHandlers.end()) - it->second(event["arg"]); - ++count; + for (int i = 0; i < pendingEvents["length"].as<int>();) { + if (!m_eventFilter(pendingEvents[i]["index"].as<int>())) { + ++i; + } else { + // Grab one event (handler and arg), and call it + emscripten::val event = pendingEvents[i]; + pendingEvents.call<void>("splice", i, 1); + + auto it = m_eventHandlers.find(event["index"].as<int>()); + if (it != m_eventHandlers.end()) + it->second(event["arg"]); + ++count; + } } - return count; -} -// Sends the pending exclusive event, and resets the "exclusive" state -int QWasmSuspendResumeControl::sendPendingExclusiveEvent() -{ - emscripten::val control = suspendResumeControlJs(); - int exclusiveHandlerIndex = control["exclusiveEventHandler"].as<int>(); - control.set("exclusiveEventHandler", 0); - emscripten::val event = control["pendingEvents"].call<val>("pop"); - int eventHandlerIndex = event["index"].as<int>(); - Q_ASSERT(exclusiveHandlerIndex == eventHandlerIndex); - auto it = m_eventHandlers.find(eventHandlerIndex); - Q_ASSERT(it != m_eventHandlers.end()); - it->second(event["arg"]); - return 1; + if (control["exclusiveEventHandler"].as<int>() > 0) { + control.set("exclusiveEventHandler", 0); + m_eventFilter = [](int) { return true;}; + } + return count; } void qtSendPendingEvents() diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h index b750d80314c..ff97ff3d7ea 100644 --- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h +++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h @@ -38,15 +38,16 @@ public: static emscripten::val suspendResumeControlJs(); void suspend(); - void suspendExclusive(uint32_t eventHandlerIndex); + // Accept events for all handlers, start to process events on last handler in list + void suspendExclusive(QList<uint32_t> eventHandlerIndices); int sendPendingEvents(); - int sendPendingExclusiveEvent(); private: friend void qtSendPendingEvents(); static QWasmSuspendResumeControl *s_suspendResumeControl; std::map<int, std::function<void(emscripten::val)>> m_eventHandlers; + std::function<bool(int)> m_eventFilter = [](int) { return true; }; }; class Q_CORE_EXPORT QWasmEventHandler diff --git a/src/corelib/text/qlocale_icu.cpp b/src/corelib/text/qlocale_icu.cpp index a10ae1c84b2..7e1dba5ee92 100644 --- a/src/corelib/text/qlocale_icu.cpp +++ b/src/corelib/text/qlocale_icu.cpp @@ -17,10 +17,10 @@ static_assert(std::is_same_v<UChar, char16_t>, namespace QtIcuPrivate { -enum class CaseConversion : bool { Upper, Lower }; +enum class IcuCaseConversion : bool { Upper, Lower }; static bool qt_u_strToCase(const QString &str, QString *out, const char *localeID, - CaseConversion conv) + IcuCaseConversion conv) { Q_ASSERT(out); @@ -34,9 +34,9 @@ static bool qt_u_strToCase(const QString &str, QString *out, const char *localeI // try to be a completely transparent wrapper: using R [[maybe_unused]] = decltype(u_strToUpper(std::forward<decltype(args)>(args)...)); switch (conv) { - case CaseConversion::Upper: + case IcuCaseConversion::Upper: return u_strToUpper(std::forward<decltype(args)>(args)...); - case CaseConversion::Lower: + case IcuCaseConversion::Lower: return u_strToLower(std::forward<decltype(args)>(args)...); }; Q_UNREACHABLE_RETURN(R{0}); @@ -79,7 +79,7 @@ QString QLocalePrivate::toUpper(const QString &str, bool *ok) const Q_ASSERT(ok); using namespace QtIcuPrivate; QString out; - *ok = qt_u_strToCase(str, &out, bcp47Name('_'), CaseConversion::Upper); + *ok = qt_u_strToCase(str, &out, bcp47Name('_'), IcuCaseConversion::Upper); return out; } @@ -88,7 +88,7 @@ QString QLocalePrivate::toLower(const QString &str, bool *ok) const Q_ASSERT(ok); using namespace QtIcuPrivate; QString out; - *ok = qt_u_strToCase(str, &out, bcp47Name('_'), CaseConversion::Lower); + *ok = qt_u_strToCase(str, &out, bcp47Name('_'), IcuCaseConversion::Lower); return out; } diff --git a/src/gui/image/qpaintengine_pic.cpp b/src/gui/image/qpaintengine_pic.cpp index ba4888cef75..b151c3b2d18 100644 --- a/src/gui/image/qpaintengine_pic.cpp +++ b/src/gui/image/qpaintengine_pic.cpp @@ -31,6 +31,7 @@ public: QDataStream s; QPainter *pt; QPicturePrivate *pic_d; + bool sizeLimitExceeded = false; }; QPicturePaintEngine::QPicturePaintEngine() @@ -68,6 +69,7 @@ bool QPicturePaintEngine::begin(QPaintDevice *pd) d->s.setVersion(d->pic_d->formatMajor); d->pic_d->pictb.open(QIODevice::WriteOnly | QIODevice::Truncate); + d->sizeLimitExceeded = false; d->s.writeRawData(qt_mfhdr_tag, 4); d->s << (quint16) 0 << (quint16) d->pic_d->formatMajor << (quint16) d->pic_d->formatMinor; d->s << (quint8) QPicturePrivate::PdcBegin << (quint8) sizeof(qint32); @@ -109,7 +111,7 @@ bool QPicturePaintEngine::end() d->s << cs; // write checksum d->pic_d->pictb.close(); setActive(false); - return true; + return !d->sizeLimitExceeded; } #define SERIALIZE_CMD(c) \ @@ -286,6 +288,19 @@ void QPicturePaintEngine::updateRenderHints(QPainter::RenderHints hints) void QPicturePaintEngine::writeCmdLength(int pos, const QRectF &r, bool corr) { Q_D(QPicturePaintEngine); + + constexpr int sizeLimit = std::numeric_limits<int>::max() - 8; // Leave room for ending bytes + if (d->sizeLimitExceeded || d->pic_d->pictb.pos() > sizeLimit) { + d->pic_d->trecs--; // Remove last command added, started by SERIALIZE_CMD + d->pic_d->pictb.seek(pos - 2); + d->pic_d->pictbData.resize(pos - 2); + if (!d->sizeLimitExceeded) { + d->sizeLimitExceeded = true; + qWarning("QPicture: size limit exceeded, will be truncated"); + } + return; + } + int newpos = d->pic_d->pictb.pos(); // new position int length = newpos - pos; QRectF br(r); diff --git a/src/gui/image/qpicture.cpp b/src/gui/image/qpicture.cpp index ac0525d7abf..db2a5fd9ba9 100644 --- a/src/gui/image/qpicture.cpp +++ b/src/gui/image/qpicture.cpp @@ -970,7 +970,8 @@ QPicture& QPicture::operator=(const QPicture &p) Constructs a QPicturePrivate */ QPicturePrivate::QPicturePrivate() - : in_memory_only(false) + : pictb(&pictbData), + in_memory_only(false) { } @@ -980,7 +981,8 @@ QPicturePrivate::QPicturePrivate() Copy-Constructs a QPicturePrivate. Needed when detaching. */ QPicturePrivate::QPicturePrivate(const QPicturePrivate &other) - : trecs(other.trecs), + : pictb(&pictbData), + trecs(other.trecs), formatOk(other.formatOk), formatMinor(other.formatMinor), brect(other.brect), diff --git a/src/gui/image/qpicture_p.h b/src/gui/image/qpicture_p.h index c512f49320b..1d8142f44b7 100644 --- a/src/gui/image/qpicture_p.h +++ b/src/gui/image/qpicture_p.h @@ -113,6 +113,7 @@ public: bool checkFormat(); void resetFormat(); + QByteArray pictbData; QBuffer pictb; int trecs; bool formatOk; diff --git a/src/gui/painting/qcolorspace.cpp b/src/gui/painting/qcolorspace.cpp index c43d133dd1e..9149971b999 100644 --- a/src/gui/painting/qcolorspace.cpp +++ b/src/gui/painting/qcolorspace.cpp @@ -1206,12 +1206,12 @@ QByteArray QColorSpace::iccProfile() const QColorSpace QColorSpace::fromIccProfile(const QByteArray &iccProfile) { // Must detach if input is fromRawData(); nullTerminated() is trick to do that and nothing else - const QByteArray ownedIccProfile(iccProfile.nullTerminated()); + QByteArray ownedIccProfile = iccProfile.nullTerminated(); QColorSpace colorSpace; if (QIcc::fromIccProfile(ownedIccProfile, &colorSpace)) return colorSpace; colorSpace.detach(); - colorSpace.d_ptr->iccProfile = ownedIccProfile; + colorSpace.d_ptr->iccProfile = std::move(ownedIccProfile); return colorSpace; } diff --git a/src/gui/painting/qpainter.cpp b/src/gui/painting/qpainter.cpp index a3f9f069b69..2b6a8a858f9 100644 --- a/src/gui/painting/qpainter.cpp +++ b/src/gui/painting/qpainter.cpp @@ -6474,8 +6474,9 @@ QRectF QPainter::boundingRect(const QRectF &r, const QString &text, const QTextO and height (on both 1x and 2x displays), and produces high-resolution output on 2x displays. - The \a position offset is always in the painter coordinate system, - indepentent of display devicePixelRatio. + The \a position offset is provided in the device independent pixels + relative to the top-left corner of the \a rectangle. The \a position + can be used to align the repeating pattern inside the \a rectangle. \sa drawPixmap() */ @@ -6497,8 +6498,8 @@ void QPainter::drawTiledPixmap(const QRectF &r, const QPixmap &pixmap, const QPo qt_painter_thread_test(d->device->devType(), d->engine->type(), "drawTiledPixmap()"); #endif - qreal sw = pixmap.width(); - qreal sh = pixmap.height(); + const qreal sw = pixmap.width() / pixmap.devicePixelRatio(); + const qreal sh = pixmap.height() / pixmap.devicePixelRatio(); qreal sx = sp.x(); qreal sy = sp.y(); if (sx < 0) @@ -6579,8 +6580,12 @@ void QPainter::drawTiledPixmap(const QRectF &r, const QPixmap &pixmap, const QPo (\a{x}, \a{y}) specifies the top-left point in the paint device that is to be drawn onto; with the given \a width and \a - height. (\a{sx}, \a{sy}) specifies the top-left point in the \a - pixmap that is to be drawn; this defaults to (0, 0). + height. + + (\a{sx}, \a{sy}) specifies the origin inside the specified rectangle + where the pixmap will be drawn. The origin position is specified in + the device independent pixels relative to (\a{x}, \a{y}). This defaults + to (0, 0). */ #ifndef QT_NO_PICTURE diff --git a/src/network/access/http2/http2frames.cpp b/src/network/access/http2/http2frames.cpp index 3b52204c7d3..e6a3474d7b0 100644 --- a/src/network/access/http2/http2frames.cpp +++ b/src/network/access/http2/http2frames.cpp @@ -34,7 +34,8 @@ FrameType Frame::type() const quint32 Frame::streamID() const { Q_ASSERT(buffer.size() >= frameHeaderSize); - return qFromBigEndian<quint32>(&buffer[5]); + // RFC 9113, 4.1: 31-bit Stream ID; lastValidStreamID(0x7FFFFFFF) masks out the reserved MSB + return qFromBigEndian<quint32>(&buffer[5]) & lastValidStreamID; } FrameFlags Frame::flags() const diff --git a/src/network/access/qhttp2connection.cpp b/src/network/access/qhttp2connection.cpp index 1d5c0d92b63..2895e8335d2 100644 --- a/src/network/access/qhttp2connection.cpp +++ b/src/network/access/qhttp2connection.cpp @@ -454,7 +454,7 @@ void QHttp2Stream::internalSendDATA() "[%p] stream %u, exhausted device %p, sent END_STREAM? %d, %ssending end stream " "after DATA", connection, m_streamID, m_uploadByteDevice, sentEND_STREAM, - m_endStreamAfterDATA ? "" : "not "); + !sentEND_STREAM && m_endStreamAfterDATA ? "" : "not "); if (!sentEND_STREAM && m_endStreamAfterDATA) { // We need to send an empty DATA frame with END_STREAM since we // have exhausted the device, but we haven't sent END_STREAM yet. @@ -690,8 +690,9 @@ void QHttp2Stream::handleDATA(const Frame &inboundFrame) m_recvWindow -= qint32(inboundFrame.payloadSize()); const bool endStream = inboundFrame.flags().testFlag(FrameFlag::END_STREAM); + const bool ignoreData = connection->streamIsIgnored(m_streamID); // Uncompress data if needed and append it ... - if (inboundFrame.dataSize() > 0 || endStream) { + if ((inboundFrame.dataSize() > 0 || endStream) && !ignoreData) { QByteArray fragment(reinterpret_cast<const char *>(inboundFrame.dataBegin()), inboundFrame.dataSize()); if (endStream) @@ -1245,16 +1246,12 @@ void QHttp2Connection::connectionError(Http2Error errorCode, const char *message { Q_ASSERT(message); // RFC 9113, 6.8: An endpoint MAY send multiple GOAWAY frames if circumstances change. - // Anyway, we do not send multiple GOAWAY frames. - if (m_goingAway) - return; qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %s (%d)", this, message, int(errorCode)); // RFC 9113, 6.8: Endpoints SHOULD always send a GOAWAY frame before closing a connection so // that the remote peer can know whether a stream has been partially processed or not. - m_goingAway = true; sendGOAWAY(errorCode); auto messageView = QLatin1StringView(message); @@ -1295,6 +1292,20 @@ bool QHttp2Connection::isInvalidStream(quint32 streamID) noexcept return (!stream || stream->wasResetbyPeer()) && !streamWasResetLocally(streamID); } +/*! + When we send a GOAWAY we also send the ID of the last stream we know about + at the time. Any stream that starts after this one is ignored, but we still + have to process HEADERS due to compression state, and DATA due to stream and + connection window size changes. + Other than that - any \a streamID for which this returns true should be + ignored, and deleted at the earliest convenience. +*/ +bool QHttp2Connection::streamIsIgnored(quint32 streamID) const noexcept +{ + const bool streamIsRemote = (streamID & 1) == (m_connectionType == Type::Client ? 0 : 1); + return Q_UNLIKELY(streamIsRemote && m_lastStreamToProcess < streamID); +} + bool QHttp2Connection::sendClientPreface() { QIODevice *socket = getSocket(); @@ -1359,9 +1370,16 @@ bool QHttp2Connection::sendWINDOW_UPDATE(quint32 streamID, quint32 delta) bool QHttp2Connection::sendGOAWAY(Http2::Http2Error errorCode) { + m_goingAway = true; + // If this is the first time, start the timer: + if (m_lastStreamToProcess == Http2::lastValidStreamID) + m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod); + m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess); + qCDebug(qHttp2ConnectionLog, "[%p] Sending GOAWAY frame, error code %u, last stream %u", this, + errorCode, m_lastStreamToProcess); frameWriter.start(FrameType::GOAWAY, FrameFlag::EMPTY, Http2PredefinedParameters::connectionStreamID); - frameWriter.append(quint32(m_lastIncomingStreamID)); + frameWriter.append(m_lastStreamToProcess); frameWriter.append(quint32(errorCode)); return frameWriter.write(*getSocket()); } @@ -1411,8 +1429,20 @@ void QHttp2Connection::handleDATA() if (stream) stream->handleDATA(inboundFrame); - if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) - emit receivedEND_STREAM(streamID); + + if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) { + const bool ignoreData = stream && streamIsIgnored(stream->streamID()); + if (!ignoreData) { + emit receivedEND_STREAM(streamID); + } else { + // Stream opened after our GOAWAY cut-off. We would just drop the + // data, but needed to handle it enough to track sizes of streams and + // connection windows. Since we've now taken care of that, we can + // at last close and delete it. + stream->setState(QHttp2Stream::State::Closed); + delete stream; + } + } if (sessionReceiveWindowSize < maxSessionReceiveWindowSize / 2) { // @future[consider]: emit signal instead @@ -1454,8 +1484,15 @@ void QHttp2Connection::handleHEADERS() return; } - qCDebug(qHttp2ConnectionLog, "[%p] Created new incoming stream %d", this, streamID); - emit newIncomingStream(newStream); + qCDebug(qHttp2ConnectionLog, "[%p] New incoming stream %d", this, streamID); + if (!streamIsIgnored(newStream->streamID())) { + emit newIncomingStream(newStream); + } else if (m_goawayGraceTimer.hasExpired()) { + // We gave the peer some time to handle the GOAWAY message, but they have started a new + // stream, so we error out. + connectionError(Http2Error::PROTOCOL_ERROR, "Peer refused to GOAWAY."); + return; + } } else if (streamWasResetLocally(streamID)) { qCDebug(qHttp2ConnectionLog, "[%p] Received HEADERS on previously locally reset stream %d (must process but ignore)", @@ -1500,6 +1537,9 @@ void QHttp2Connection::handlePRIORITY() || inboundFrame.type() == FrameType::HEADERS); const auto streamID = inboundFrame.streamID(); + if (streamIsIgnored(streamID)) + return; + // RFC 9913, 6.3: If a PRIORITY frame is received with a stream identifier of 0x00, the // recipient MUST respond with a connection error if (streamID == connectionStreamID) @@ -1534,11 +1574,14 @@ void QHttp2Connection::handleRST_STREAM() { Q_ASSERT(inboundFrame.type() == FrameType::RST_STREAM); + const auto streamID = inboundFrame.streamID(); + if (streamIsIgnored(streamID)) + return; + // RFC 9113, 6.4: RST_STREAM frames MUST be associated with a stream. // If a RST_STREAM frame is received with a stream identifier of 0x0, // the recipient MUST treat this as a connection error (Section 5.4.1) // of type PROTOCOL_ERROR. - const auto streamID = inboundFrame.streamID(); if (streamID == connectionStreamID) return connectionError(PROTOCOL_ERROR, "RST_STREAM on 0x0"); @@ -1764,31 +1807,33 @@ void QHttp2Connection::handleGOAWAY() Q_ASSERT(inboundFrame.payloadSize() >= 8); const uchar *const src = inboundFrame.dataBegin(); - quint32 lastStreamID = qFromBigEndian<quint32>(src); + // RFC 9113, 4.1: 31-bit Stream ID; lastValidStreamID(0x7FFFFFFF) masks out the reserved MSB + const quint32 lastStreamID = qFromBigEndian<quint32>(src) & lastValidStreamID; const Http2Error errorCode = Http2Error(qFromBigEndian<quint32>(src + 4)); - if (!lastStreamID) { - // "The last stream identifier can be set to 0 if no - // streams were processed." - lastStreamID = 1; - } else if (!(lastStreamID & 0x1)) { - // 5.1.1 - we (client) use only odd numbers as stream identifiers. + // 6.8 "the GOAWAY contains the stream identifier of the last peer-initiated stream that was + // or might be processed on the sending endpoint in this connection." + // Alternatively, they can specify 0 as the last stream ID, meaning they are not intending to + // process any remaining stream(s). + const quint32 LocalMask = m_connectionType == Type::Client ? 1 : 0; + // The stream must match the LocalMask, meaning we initiated it, for the last stream ID to make + // sense - they are not processing their own streams. + if (lastStreamID != 0 && (lastStreamID & 0x1) != LocalMask) return connectionError(PROTOCOL_ERROR, "GOAWAY with invalid last stream ID"); - } else if (lastStreamID >= m_nextStreamID) { - // "A server that is attempting to gracefully shut down a connection SHOULD - // send an initial GOAWAY frame with the last stream identifier set to 2^31-1 - // and a NO_ERROR code." - if (lastStreamID != lastValidStreamID || errorCode != HTTP2_NO_ERROR) - return connectionError(PROTOCOL_ERROR, "GOAWAY invalid stream/error code"); - } else { - lastStreamID += 2; - } + qCDebug(qHttp2ConnectionLog, "[%p] Received GOAWAY frame, error code %u, last stream %u", + this, errorCode, lastStreamID); m_goingAway = true; emit receivedGOAWAY(errorCode, lastStreamID); - for (quint32 id = lastStreamID; id < m_nextStreamID; id += 2) { + // Since the embedded stream ID is the last one that was or _might be_ processed, + // we cancel anything that comes after it. 0 can be used in the special case that + // no streams at all were or will be processed. + const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2; + const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream; + Q_ASSERT((firstCancelledStream & 0x1) == LocalMask); + for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) { QHttp2Stream *stream = m_streams.value(id, nullptr); if (stream && stream->isActive()) stream->finishWithError(errorCode, "Received GOAWAY"_L1); @@ -1809,7 +1854,8 @@ void QHttp2Connection::handleWINDOW_UPDATE() // errors on the connection flow-control window MUST be treated as a connection error const bool valid = delta && delta <= quint32(std::numeric_limits<qint32>::max()); const auto streamID = inboundFrame.streamID(); - + if (streamIsIgnored(streamID)) + return; // RFC 9113, 6.9: A WINDOW_UPDATE frame with a length other than 4 octets MUST be treated // as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR. @@ -1939,6 +1985,18 @@ void QHttp2Connection::handleContinuedHEADERS() if (streamWasResetLocally(streamID) || streamIt == m_streams.cend()) return; // No more processing without a stream from here on. + if (streamIsIgnored(streamID)) { + // Stream was established after GOAWAY cut-off, we ignore it, but we + // have to process things that alter state. That already happened, so we + // stop here. + if (continuedFrames[0].flags().testFlag(Http2::FrameFlag::END_STREAM)) { + if (QHttp2Stream *stream = streamIt.value()) { + stream->setState(QHttp2Stream::State::Closed); + delete stream; + } + } + return; + } switch (firstFrameType) { case FrameType::HEADERS: diff --git a/src/network/access/qhttp2connection_p.h b/src/network/access/qhttp2connection_p.h index dcdc0f91318..f3f14145278 100644 --- a/src/network/access/qhttp2connection_p.h +++ b/src/network/access/qhttp2connection_p.h @@ -283,6 +283,8 @@ private: bool isInvalidStream(quint32 streamID) noexcept; bool streamWasResetLocally(quint32 streamID) noexcept; + Q_ALWAYS_INLINE + bool streamIsIgnored(quint32 streamID) const noexcept; void connectionError(Http2::Http2Error errorCode, const char *message); // Connection failed to be established? @@ -400,6 +402,10 @@ private: bool m_goingAway = false; bool pushPromiseEnabled = false; quint32 m_lastIncomingStreamID = Http2::connectionStreamID; + // Gets lowered when/if we send GOAWAY: + quint32 m_lastStreamToProcess = Http2::lastValidStreamID; + static constexpr std::chrono::duration GoawayGracePeriod = std::chrono::seconds(60); + QDeadlineTimer m_goawayGraceTimer; bool m_prefaceSent = false; diff --git a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm index 7644867700a..7ca3e61dfa5 100644 --- a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm +++ b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm @@ -25,6 +25,8 @@ #include <qpa/qwindowsysteminterface.h> #include <qwindowdefs.h> +#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h> + QT_USE_NAMESPACE @implementation QCocoaApplicationDelegate { @@ -194,6 +196,23 @@ QT_USE_NAMESPACE QCocoaMenuBar::insertWindowMenu(); } +/*! + Tells the delegate to open the specified files + + Sent by the system when the user drags a file to the app's icon + in places like Finder or the Dock, or opens a file via the "Open + With" menu in Finder. + + These actions can happen when the application is not running, + in which case the call comes in between willFinishLaunching + and didFinishLaunching. In this case we don't pass on the + incoming file paths as file open events, as the paths are + also part of the command line arguments, and Qt applications + normally expect to handle file opening via those. + + \note The app must register itself as a handler for each file + type via the CFBundleDocumentTypes key in the Info.plist. + */ - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames { Q_UNUSED(filenames); @@ -209,7 +228,10 @@ QT_USE_NAMESPACE if (qApp->arguments().contains(qtFileName)) continue; } - QWindowSystemInterface::handleFileOpenEvent(qtFileName); + QUrl url = qt_apple_urlFromPossiblySecurityScopedURL([NSURL fileURLWithPath:fileName]); + QWindowSystemInterface::handleFileOpenEvent(url); + // FIXME: We're supposed to call [NSApp replyToOpenOrPrint:] here, but we + // don't know if the open operation succeeded, failed, or was cancelled. } if ([reflectionDelegate respondsToSelector:_cmd]) @@ -262,6 +284,17 @@ QT_USE_NAMESPACE } } +/*! + Returns a Boolean value that indicates if the app responds + to reopen AppleEvents. + + These events are sent whenever the Finder reactivates an already + running application because someone double-clicked it again or used + the dock to activate it. + + We pass the activation on to Qt, and return YES, to let AppKit + follow its normal flow. + */ - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag { if ([reflectionDelegate respondsToSelector:_cmd]) @@ -309,6 +342,25 @@ QT_USE_NAMESPACE [self doesNotRecognizeSelector:invocationSelector]; } +/*! + Callback for when the application is asked to pick up a user activity + from another app (also known as Handoff, which is part of the bigger + Continuity story for Apple operating systems). + + This is normally managed by two apps by the same vendor explicitly + initiating a custom NSUserActivity and picking it up in another app + on the same or another device, which we don't have APIs for. + + This is also how the system supports Universal Links, where a web page + can deep-link into an app. In this case the app needs to claim and + validate an associated domain. The resulting link will be delivered + as a special NSUserActivityTypeBrowsingWeb activity type, which we + treat as QDesktopServices::handleUrl(). + + Finally, for NS/UIDocument based apps (which Qt is not), the system + automatically handles document hand-off if the application includes + the NSUbiquitousDocumentUserActivityType key in its Info.plist. + */ - (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray<id<NSUserActivityRestoring>> *restorableObjects))restorationHandler { @@ -331,6 +383,18 @@ QT_USE_NAMESPACE return NO; } +/*! + Callback for when the app is asked to open custom URL schemes. + + We register a handler for events of type kInternetEventClass with the + NSAppleEventManager during application start. + + The application must include the schemes in the CFBundleURLTypes + key of the Info.plist. + + \note This callback is not used for http/https URLs, see + continueUserActivity above for how we handle that. + */ - (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { Q_UNUSED(replyEvent); diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm index 4c4e5fac962..a79682e4e14 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm @@ -19,6 +19,7 @@ #include <QtCore/qregularexpression.h> #include <QtCore/qpointer.h> #include <QtCore/private/qcore_mac_p.h> +#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h> #include <QtGui/qguiapplication.h> #include <QtGui/private/qguiapplication_p.h> @@ -395,14 +396,15 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; { if (auto *openPanel = openpanel_cast(m_panel)) { QList<QUrl> result; - for (NSURL *url in openPanel.URLs) { - QString path = QString::fromNSString(url.path).normalized(QString::NormalizationForm_C); - result << QUrl::fromLocalFile(path); - } + for (NSURL *url in openPanel.URLs) + result << qt_apple_urlFromPossiblySecurityScopedURL(url); return result; } else { - QString filename = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); - QFileInfo fileInfo(filename); + QUrl result = qt_apple_urlFromPossiblySecurityScopedURL(m_panel.URL); + if (qt_apple_isSandboxed()) + return { result }; // Can't tweak suffix + + QFileInfo fileInfo(result.toLocalFile()); if (fileInfo.suffix().isEmpty() && ![self fileInfoMatchesCurrentNameFilter:fileInfo]) { // We end up in this situation if we accept a file name without extension @@ -733,6 +735,26 @@ bool QCocoaFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModalit return false; } + if (qt_apple_isSandboxed()) { + static bool canRead = qt_mac_processHasEntitlement( + u"com.apple.security.files.user-selected.read-only"_s); + static bool canReadWrite = qt_mac_processHasEntitlement( + u"com.apple.security.files.user-selected.read-write"_s); + + if (options()->acceptMode() == QFileDialogOptions::AcceptSave + && !canReadWrite) { + qWarning() << "Sandboxed application is missing user-selected files" + << "read-write entitlement. Falling back to non-native dialog"; + return false; + } + + if (!canReadWrite && !canRead) { + qWarning() << "Sandboxed application is missing user-selected files" + << "entitlement. Falling back to non-native dialog"; + return false; + } + } + createNSOpenSavePanelDelegate(); return [m_delegate showPanel:windowModality withParent:parent]; diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.mm b/src/plugins/platforms/ios/qiosapplicationdelegate.mm index 380c5a588e6..7cbb4fc40f5 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.mm +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.mm @@ -16,6 +16,8 @@ #include <QtCore/QtCore> +#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h> + @interface QIOSWindowSceneDelegate : NSObject<UIWindowSceneDelegate> @property (nullable, nonatomic, strong) UIWindow *window; @end @@ -112,7 +114,7 @@ QIOSServices *iosServices = static_cast<QIOSServices *>(iosIntegration->services()); for (UIOpenURLContext *urlContext in URLContexts) { - QUrl url = QUrl::fromNSURL(urlContext.URL); + QUrl url = qt_apple_urlFromPossiblySecurityScopedURL(urlContext.URL); if (url.isLocalFile()) QWindowSystemInterface::handleFileOpenEvent(url); else diff --git a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm index c173aa426fc..57a5e100c9e 100644 --- a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm +++ b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm @@ -8,6 +8,7 @@ #include "qiosdocumentpickercontroller.h" #include <QtCore/qpointer.h> +#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h> @implementation QIOSDocumentPickerController { QPointer<QIOSFileDialog> m_fileDialog; @@ -17,9 +18,11 @@ { NSMutableArray <UTType *> *docTypes = [[[NSMutableArray alloc] init] autorelease]; - QStringList nameFilters = fileDialog->options()->nameFilters(); - if (!nameFilters.isEmpty() && (fileDialog->options()->fileMode() != QFileDialogOptions::Directory - || fileDialog->options()->fileMode() != QFileDialogOptions::DirectoryOnly)) + const auto options = fileDialog->options(); + + const QStringList nameFilters = options->nameFilters(); + if (!nameFilters.isEmpty() && (options->fileMode() != QFileDialogOptions::Directory + || options->fileMode() != QFileDialogOptions::DirectoryOnly)) { QStringList results; for (const QString &filter : nameFilters) @@ -28,21 +31,8 @@ docTypes = [self computeAllowedFileTypes:results]; } - // FIXME: Handle security scoped URLs instead of copying resource - bool asCopy = [&]{ - switch (fileDialog->options()->fileMode()) { - case QFileDialogOptions::AnyFile: - case QFileDialogOptions::ExistingFile: - case QFileDialogOptions::ExistingFiles: - return true; - default: - // Folders can't be imported - return false; - } - }(); - if (!docTypes.count) { - switch (fileDialog->options()->fileMode()) { + switch (options->fileMode()) { case QFileDialogOptions::AnyFile: case QFileDialogOptions::ExistingFile: case QFileDialogOptions::ExistingFiles: @@ -58,17 +48,39 @@ } } - if (self = [super initForOpeningContentTypes:docTypes asCopy:asCopy]) { - m_fileDialog = fileDialog; - self.modalPresentationStyle = UIModalPresentationFormSheet; - self.delegate = self; - self.presentationController.delegate = self; + if (options->acceptMode() == QFileDialogOptions::AcceptSave) { + auto selectedUrls = options->initiallySelectedFiles(); + auto suggestedFileName = !selectedUrls.isEmpty() ? selectedUrls.first().fileName() : "Untitled"; - if (m_fileDialog->options()->fileMode() == QFileDialogOptions::ExistingFiles) + // Create an empty dummy file, so that the export dialog will allow us + // to choose the export destination, which we are then given access to + // write to. + NSURL *dummyExportFile = [NSFileManager.defaultManager.temporaryDirectory + URLByAppendingPathComponent:suggestedFileName.toNSString()]; + [NSFileManager.defaultManager createFileAtPath:dummyExportFile.path contents:nil attributes:nil]; + + if (!(self = [super initForExportingURLs:@[dummyExportFile]])) + return nil; + + // Note, we don't set the directoryURL, as if the directory can't be + // accessed, or written to, the file dialog is shown but is empty. + // FIXME: See comment below for open dialogs as well + } else { + if (!(self = [super initForOpeningContentTypes:docTypes asCopy:NO])) + return nil; + + if (options->fileMode() == QFileDialogOptions::ExistingFiles) self.allowsMultipleSelection = YES; - self.directoryURL = m_fileDialog->options()->initialDirectory().toNSURL(); + // FIXME: This doesn't seem to have any effect + self.directoryURL = options->initialDirectory().toNSURL(); } + + m_fileDialog = fileDialog; + self.modalPresentationStyle = UIModalPresentationFormSheet; + self.delegate = self; + self.presentationController.delegate = self; + return self; } @@ -81,7 +93,7 @@ QList<QUrl> files; for (NSURL* url in urls) - files.append(QUrl::fromNSURL(url)); + files.append(qt_apple_urlFromPossiblySecurityScopedURL(url)); m_fileDialog->selectedFilesChanged(files); emit m_fileDialog->accept(); diff --git a/src/plugins/platforms/ios/qiosfiledialog.mm b/src/plugins/platforms/ios/qiosfiledialog.mm index b7d3e488bbb..6e7c10117ed 100644 --- a/src/plugins/platforms/ios/qiosfiledialog.mm +++ b/src/plugins/platforms/ios/qiosfiledialog.mm @@ -49,14 +49,10 @@ bool QIOSFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality window // when converted to QUrl, it becames a scheme. const QString scheme = initialDir.scheme(); - if (acceptOpen) { - if (directory.startsWith("assets-library:"_L1) || scheme == "assets-library"_L1) - return showImagePickerDialog(parent); - else - return showNativeDocumentPickerDialog(parent); - } + if (acceptOpen && (directory.startsWith("assets-library:"_L1) || scheme == "assets-library"_L1)) + return showImagePickerDialog(parent); - return false; + return showNativeDocumentPickerDialog(parent); } void QIOSFileDialog::showImagePickerDialog_helper(QWindow *parent) diff --git a/src/plugins/platforms/windows/qwindowsscreen.cpp b/src/plugins/platforms/windows/qwindowsscreen.cpp index 2bd2f0c9e3d..0236669d6fb 100644 --- a/src/plugins/platforms/windows/qwindowsscreen.cpp +++ b/src/plugins/platforms/windows/qwindowsscreen.cpp @@ -704,7 +704,7 @@ void QWindowsScreenManager::initialize() qCDebug(lcQpaScreen) << "Initializing screen manager"; auto className = QWindowsWindowClassRegistry::instance()->registerWindowClass( - QLatin1String("ScreenChangeObserverWindow"), + "ScreenChangeObserverWindow"_L1, qDisplayChangeObserverWndProc); // HWND_MESSAGE windows do not get WM_DISPLAYCHANGE, so we need to create diff --git a/src/plugins/platforms/windows/qwindowstheme.cpp b/src/plugins/platforms/windows/qwindowstheme.cpp index d132bbb6130..b9f60f7713c 100644 --- a/src/plugins/platforms/windows/qwindowstheme.cpp +++ b/src/plugins/platforms/windows/qwindowstheme.cpp @@ -549,7 +549,7 @@ QWindowsTheme::QWindowsTheme() refreshIconPixmapSizes(); auto className = QWindowsWindowClassRegistry::instance()->registerWindowClass( - QLatin1String("ThemeChangeObserverWindow"), + "ThemeChangeObserverWindow"_L1, qThemeChangeObserverWndProc); // HWND_MESSAGE windows do not get the required theme events, // so we use a real top-level window that we never show. diff --git a/src/plugins/platforms/windows/qwindowswindow.cpp b/src/plugins/platforms/windows/qwindowswindow.cpp index ed391009423..b77e985c965 100644 --- a/src/plugins/platforms/windows/qwindowswindow.cpp +++ b/src/plugins/platforms/windows/qwindowswindow.cpp @@ -61,6 +61,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + using QWindowCreationContextPtr = QSharedPointer<QWindowCreationContext>; enum { @@ -889,7 +891,7 @@ QWindowsWindowData const QString windowClassName = QWindowsWindowClassRegistry::instance()->registerWindowClass(w); QWindowsWindowClassDescription windowTitlebarDescription; - windowTitlebarDescription.name = QStringLiteral("_q_titlebar"); + windowTitlebarDescription.name = "_q_titlebar"_L1; windowTitlebarDescription.style = CS_VREDRAW | CS_HREDRAW; windowTitlebarDescription.shouldAddPrefix = false; const QString windowTitlebarName = QWindowsWindowClassRegistry::instance()->registerWindowClass(windowTitlebarDescription); diff --git a/src/plugins/styles/modernwindows/qwindows11style.cpp b/src/plugins/styles/modernwindows/qwindows11style.cpp index bf4b3c6a9bc..ffba4f775c8 100644 --- a/src/plugins/styles/modernwindows/qwindows11style.cpp +++ b/src/plugins/styles/modernwindows/qwindows11style.cpp @@ -24,6 +24,7 @@ #if QT_CONFIG(mdiarea) #include <QtWidgets/qmdiarea.h> #endif +#include <QtWidgets/qplaintextedit.h> #include <QtWidgets/qtextedit.h> #include <QtWidgets/qtreeview.h> #if QT_CONFIG(datetimeedit) @@ -1027,7 +1028,9 @@ void QWindows11Style::drawPrimitive(PrimitiveElement element, const QStyleOption if (frame->frameShape == QFrame::NoFrame) break; - drawLineEditFrame(painter, rect, option, qobject_cast<const QTextEdit *>(widget) != nullptr); + const bool isEditable = qobject_cast<const QTextEdit *>(widget) != nullptr + || qobject_cast<const QPlainTextEdit *>(widget) != nullptr; + drawLineEditFrame(painter, rect, option, isEditable); } break; } @@ -1452,36 +1455,10 @@ void QWindows11Style::drawControl(ControlElement element, const QStyleOption *op #endif // QT_CONFIG(progressbar) case CE_PushButtonLabel: if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) { - using namespace StyleOptionHelper; - const bool isEnabled = !isDisabled(option); - - QRect textRect = btn->rect.marginsRemoved(QMargins(contentHMargin, 0, contentHMargin, 0)); - int tf = Qt::AlignCenter | Qt::TextShowMnemonic; - if (!proxy()->styleHint(SH_UnderlineShortcut, btn, widget)) - tf |= Qt::TextHideMnemonic; - - if (!btn->icon.isNull()) { - //Center both icon and text - QIcon::Mode mode = isEnabled ? QIcon::Normal : QIcon::Disabled; - if (mode == QIcon::Normal && btn->state & State_HasFocus) - mode = QIcon::Active; - QIcon::State state = isChecked(btn) ? QIcon::On : QIcon::Off; - - int iconSpacing = 4;//### 4 is currently hardcoded in QPushButton::sizeHint() - - QRect iconRect = QRect(textRect.x(), textRect.y(), btn->iconSize.width(), textRect.height()); - QRect vIconRect = visualRect(btn->direction, btn->rect, iconRect); - textRect.setLeft(textRect.left() + iconRect.width() + iconSpacing); - - if (isChecked(btn) || isPressed(btn)) - vIconRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget), - proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget)); - btn->icon.paint(painter, vIconRect, Qt::AlignCenter, mode, state); - } - - auto vTextRect = visualRect(btn->direction, btn->rect, textRect); - painter->setPen(controlTextColor(option)); - proxy()->drawItemText(painter, vTextRect, tf, option->palette, isEnabled, btn->text); + QStyleOptionButton btnCopy(*btn); + btnCopy.rect = btn->rect.marginsRemoved(QMargins(contentHMargin, 0, contentHMargin, 0)); + btnCopy.palette.setBrush(QPalette::ButtonText, controlTextColor(option)); + QCommonStyle::drawControl(element, &btnCopy, painter, widget); } break; case CE_PushButtonBevel: @@ -2625,7 +2602,7 @@ QIcon QWindows11Style::standardIcon(StandardPixmap standardIcon, switch (standardIcon) { case SP_LineEditClearButton: { if (d->m_lineEditClearButton.isNull()) { - auto e = new WinFontIconEngine(Clear.at(0), d->assetFont); + auto e = new WinFontIconEngine(Clear, d->assetFont); d->m_lineEditClearButton = QIcon(e); } return d->m_lineEditClearButton; |
