summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/corelib/CMakeLists.txt1
-rw-r--r--src/corelib/doc/qtcore.qdocconf7
-rw-r--r--src/corelib/doc/src/qtcore.qdoc9
-rw-r--r--src/corelib/io/qfile.cpp13
-rw-r--r--src/corelib/io/qfilesystemengine.cpp8
-rw-r--r--src/corelib/io/qfilesystemengine_p.h2
-rw-r--r--src/corelib/io/qfsfileengine_p.h2
-rw-r--r--src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm552
-rw-r--r--src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h29
-rw-r--r--src/corelib/platform/wasm/qstdweb.cpp51
-rw-r--r--src/corelib/platform/wasm/qstdweb_p.h21
-rw-r--r--src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp54
-rw-r--r--src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h5
-rw-r--r--src/corelib/text/qlocale_icu.cpp12
-rw-r--r--src/gui/image/qpaintengine_pic.cpp17
-rw-r--r--src/gui/image/qpicture.cpp6
-rw-r--r--src/gui/image/qpicture_p.h1
-rw-r--r--src/gui/painting/qcolorspace.cpp4
-rw-r--r--src/gui/painting/qpainter.cpp17
-rw-r--r--src/network/access/http2/http2frames.cpp3
-rw-r--r--src/network/access/qhttp2connection.cpp118
-rw-r--r--src/network/access/qhttp2connection_p.h6
-rw-r--r--src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm66
-rw-r--r--src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm34
-rw-r--r--src/plugins/platforms/ios/qiosapplicationdelegate.mm4
-rw-r--r--src/plugins/platforms/ios/qiosdocumentpickercontroller.mm62
-rw-r--r--src/plugins/platforms/ios/qiosfiledialog.mm10
-rw-r--r--src/plugins/platforms/windows/qwindowsscreen.cpp2
-rw-r--r--src/plugins/platforms/windows/qwindowstheme.cpp2
-rw-r--r--src/plugins/platforms/windows/qwindowswindow.cpp4
-rw-r--r--src/plugins/styles/modernwindows/qwindows11style.cpp41
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;