diff options
Diffstat (limited to 'tests')
14 files changed, 1061 insertions, 4 deletions
diff --git a/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp b/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp index 9be046c75be..b05a055252b 100644 --- a/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp +++ b/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp @@ -397,6 +397,7 @@ private slots: void iterateAssociativeContainerElements_data(); void iterateAssociativeContainerElements() { runTestFunction(); } void iterateContainerElements(); + void emptyContainerInterface(); void pairElements_data(); void pairElements() { runTestFunction(); } @@ -5324,6 +5325,26 @@ void tst_QVariant::iterateContainerElements() } } +void tst_QVariant::emptyContainerInterface() +{ + // An empty container interface should implicitly be of invalid size + // and its begin and end iterators should be equal. + + const QtMetaContainerPrivate::QMetaContainerInterface emptyContainerInterface {}; + QIterable emptyIterable(QMetaContainer(&emptyContainerInterface), nullptr); + + QCOMPARE(emptyIterable.size(), -1); + auto constBegin = emptyIterable.constBegin(); + auto constEnd = emptyIterable.constEnd(); + QVERIFY(constBegin == constEnd); + QCOMPARE(constEnd - constBegin, 0); + + auto mutableBegin = emptyIterable.mutableBegin(); + auto mutableEnd = emptyIterable.mutableEnd(); + QVERIFY(mutableBegin == mutableEnd); + QCOMPARE(mutableEnd - mutableBegin, 0); +} + template <typename Pair> static void testVariantPairElements() { QFETCH(std::function<void(void *)>, makeValue); diff --git a/tests/auto/gui/image/qimagereader/images/image16.pgm b/tests/auto/gui/image/qimagereader/images/image16.pgm new file mode 100644 index 00000000000..4e0b55131b0 --- /dev/null +++ b/tests/auto/gui/image/qimagereader/images/image16.pgm @@ -0,0 +1,260 @@ +P2 +16 +16 +65535 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +65535 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 +32767 diff --git a/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp b/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp index de9fea78ea6..8ccaf435f0b 100644 --- a/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp +++ b/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp @@ -610,6 +610,7 @@ void tst_QImageReader::imageFormat_data() QTest::newRow("pbm") << QString("image.pbm") << QByteArray("pbm") << QImage::Format_Mono; QTest::newRow("pgm") << QString("image.pgm") << QByteArray("pgm") << QImage::Format_Grayscale8; + QTest::newRow("pgm") << QString("image16.pgm") << QByteArray("pgm") << QImage::Format_Grayscale16; QTest::newRow("ppm-1") << QString("image.ppm") << QByteArray("ppm") << QImage::Format_RGB32; QTest::newRow("ppm-2") << QString("teapot.ppm") << QByteArray("ppm") << QImage::Format_RGB32; QTest::newRow("ppm-3") << QString("runners.ppm") << QByteArray("ppm") << QImage::Format_RGB32; diff --git a/tests/auto/gui/kernel/qwindow/BLACKLIST b/tests/auto/gui/kernel/qwindow/BLACKLIST index 1ef54f0bfbf..55003c7ec18 100644 --- a/tests/auto/gui/kernel/qwindow/BLACKLIST +++ b/tests/auto/gui/kernel/qwindow/BLACKLIST @@ -25,5 +25,6 @@ android windows-10 windows-11 android +macos-26 # QTBUG-142157 [stateChangeSignal] macos # QTBUG-140388 diff --git a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp index 8e8c90e14de..417655c31d9 100644 --- a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp +++ b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp @@ -8,6 +8,8 @@ #include <QtNetwork/private/hpack_p.h> #include <QtNetwork/private/bitstreams_p.h> +#include <QtCore/qregularexpression.h> + #include <limits> using namespace Qt::StringLiterals; @@ -35,6 +37,8 @@ private slots: void connectToServer(); void WINDOW_UPDATE(); void testCONTINUATIONFrame(); + void goaway_data(); + void goaway(); private: enum PeerType { Client, Server }; @@ -1051,6 +1055,112 @@ void tst_QHttp2Connection::testCONTINUATIONFrame() } } +void tst_QHttp2Connection::goaway_data() +{ + QTest::addColumn<bool>("endStreamOnHEADERS"); + QTest::addColumn<bool>("createNewStreamAfterDelay"); + QTest::addRow("end-on-headers") << true << false; + QTest::addRow("end-after-data") << false << false; + QTest::addRow("end-after-new-late-stream") << false << true; +} + +void tst_QHttp2Connection::goaway() +{ + QFETCH(const bool, endStreamOnHEADERS); + QFETCH(const bool, createNewStreamAfterDelay); + auto [client, server] = makeFakeConnectedSockets(); + auto connection = makeHttp2Connection(client.get(), {}, Client); + auto serverConnection = makeHttp2Connection(server.get(), {}, Server); + + QHttp2Stream *clientStream = connection->createStream().unwrap(); + QVERIFY(clientStream); + QVERIFY(waitForSettingsExchange(connection, serverConnection)); + + QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream }; + + QSignalSpy clientIncomingStreamSpy{ connection, &QHttp2Connection::newIncomingStream }; + QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived }; + QSignalSpy clientGoawaySpy{ connection, &QHttp2Connection::receivedGOAWAY }; + + const HPack::HttpHeader headers = getRequiredHeaders(); + clientStream->sendHEADERS(headers, false); + + QVERIFY(newIncomingStreamSpy.wait()); + auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>(); + QVERIFY(serverStream); + QVERIFY(serverConnection->sendGOAWAY(Http2::CANCEL)); + auto createStreamResult = serverConnection->createLocalStreamInternal(); + QVERIFY(createStreamResult.has_error()); + QCOMPARE(createStreamResult.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY); + + QVERIFY(clientGoawaySpy.wait()); + QCOMPARE(clientGoawaySpy.size(), 1); + // The error code used: + QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::CANCEL); + // Last ID that will be processed + QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID()); + clientGoawaySpy.clear(); + + // Test that creating a stream the normal way results in an error: + QH2Expected<QHttp2Stream *, QHttp2Connection::CreateStreamError> + invalidStream = connection->createStream(); + QVERIFY(!invalidStream.ok()); + QVERIFY(invalidStream.has_error()); + QCOMPARE(invalidStream.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY); + + // Directly create a stream to avoid the GOAWAY check: + quint32 nextStreamId = clientStream->streamID() + 2; + QHttp2Stream *secondClientStream = connection->createStreamInternal_impl(nextStreamId); + QSignalSpy streamResetSpy{ secondClientStream, &QHttp2Stream::rstFrameReceived }; + secondClientStream->sendHEADERS(headers, endStreamOnHEADERS); + // The stream should be ignored: + using namespace std::chrono_literals; + QVERIFY(!streamResetSpy.wait(100ms)); // We don't get reset because we are ignored + if (endStreamOnHEADERS) + return; + + secondClientStream->sendDATA("my data", createNewStreamAfterDelay); + // We cheat and try to send data after the END_STREAM flag has been sent + if (!createNewStreamAfterDelay) { + // Manually send a frame with END_STREAM so the QHttp2Stream thinks it's fine to send more + // DATA + connection->frameWriter.start(Http2::FrameType::DATA, Http2::FrameFlag::END_STREAM, + secondClientStream->streamID()); + connection->frameWriter.write(*connection->getSocket()); + QVERIFY(!streamResetSpy.wait(100ms)); // We don't get reset because we are ignored + + // Even without the GOAWAY this should fail (more activity after END_STREAM) + secondClientStream->sendDATA("my data", true); + QTest::ignoreMessage(QtCriticalMsg, + QRegularExpression(u".*Connection error: DATA on invalid stream.*"_s)); + QVERIFY(clientGoawaySpy.wait()); + QCOMPARE(clientGoawaySpy.size(), 1); + QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), + Http2::ENHANCE_YOUR_CALM); + QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID()); + return; // connection is dead by now + } + + // Override the deadline timer so we don't have to wait too long + serverConnection->m_goawayGraceTimer.setRemainingTime(50ms); + + // We can create the stream whenever, it is not noticed by the server until we send something. + nextStreamId += 2; + QHttp2Stream *rejectedStream = connection->createStreamInternal_impl(nextStreamId); + // Sleep until the grace period is over: + QTRY_VERIFY(serverConnection->m_goawayGraceTimer.hasExpired()); + + QVERIFY(rejectedStream->sendHEADERS(headers, true)); + + QTest::ignoreMessage(QtCriticalMsg, + QRegularExpression(u".*Connection error: Peer refused to GOAWAY\\..*"_s)); + QVERIFY(clientGoawaySpy.wait()); + QCOMPARE(clientGoawaySpy.size(), 1); + QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::PROTOCOL_ERROR); + // The first stream is still the last processed one: + QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID()); +} + QTEST_MAIN(tst_QHttp2Connection) #include "tst_qhttp2connection.moc" diff --git a/tests/auto/tools/moc/allmocs_baseline_in.json b/tests/auto/tools/moc/allmocs_baseline_in.json index 363ade3d53c..d8e6c4df538 100644 --- a/tests/auto/tools/moc/allmocs_baseline_in.json +++ b/tests/auto/tools/moc/allmocs_baseline_in.json @@ -2501,7 +2501,7 @@ { "isClass": false, "isFlag": false, - "lineNumber": 14, + "lineNumber": 13, "name": "SomeEnum", "values": [ "SomeEnumValue" diff --git a/tests/auto/tools/moc/related-metaobjects-in-namespaces.h b/tests/auto/tools/moc/related-metaobjects-in-namespaces.h index efd82107673..2513094ed0c 100644 --- a/tests/auto/tools/moc/related-metaobjects-in-namespaces.h +++ b/tests/auto/tools/moc/related-metaobjects-in-namespaces.h @@ -9,9 +9,9 @@ namespace QTBUG_2151 { class A : public QObject { Q_OBJECT - Q_ENUMS(SomeEnum) public: enum SomeEnum { SomeEnumValue = 0 }; + Q_ENUM(SomeEnum) }; class B : public QObject diff --git a/tests/auto/tools/moc/related-metaobjects-name-conflict.h b/tests/auto/tools/moc/related-metaobjects-name-conflict.h index cccd97e4e74..d88826f696a 100644 --- a/tests/auto/tools/moc/related-metaobjects-name-conflict.h +++ b/tests/auto/tools/moc/related-metaobjects-name-conflict.h @@ -9,15 +9,15 @@ #define DECLARE_GADGET_AND_OBJECT_CLASSES \ class Gadget { \ Q_GADGET \ - Q_ENUMS(SomeEnum) \ public: \ enum SomeEnum { SomeEnumValue = 0 }; \ + Q_ENUM(SomeEnum) \ }; \ class Object : public QObject{ \ Q_OBJECT \ - Q_ENUMS(SomeEnum) \ public: \ enum SomeEnum { SomeEnumValue = 0 }; \ + Q_ENUM(SomeEnum) \ }; #define DECLARE_DEPENDING_CLASSES \ diff --git a/tests/auto/widgets/kernel/qwidget/BLACKLIST b/tests/auto/widgets/kernel/qwidget/BLACKLIST index dd2cb1dcee9..9651c1480c8 100644 --- a/tests/auto/widgets/kernel/qwidget/BLACKLIST +++ b/tests/auto/widgets/kernel/qwidget/BLACKLIST @@ -41,6 +41,9 @@ android android [hoverPosition] macos-14 x86 +macos-26 # QTBUG-142157 # QTBUG-124291 [setParentChangesFocus:make dialog parentless, after] android +[enterLeaveOnWindowShowHide] +macos-26 # QTBUG-142157 diff --git a/tests/manual/sandboxed_file_access/CMakeLists.txt b/tests/manual/sandboxed_file_access/CMakeLists.txt new file mode 100644 index 00000000000..8df09401cf9 --- /dev/null +++ b/tests/manual/sandboxed_file_access/CMakeLists.txt @@ -0,0 +1,71 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_manual_sandboxed_file_access LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_standard_project_setup() + +qt_add_executable(tst_manual_sandboxed_file_access + tst_sandboxed_file_access.cpp +) + +target_link_libraries(tst_manual_sandboxed_file_access PRIVATE + Qt::CorePrivate + Qt::Widgets + Qt::Test +) + +enable_language(OBJCXX) +set_source_files_properties(tst_sandboxed_file_access.cpp PROPERTIES LANGUAGE OBJCXX) + +if(MACOS) + target_sources(tst_manual_sandboxed_file_access PRIVATE app.entitlements) + set_target_properties(tst_manual_sandboxed_file_access PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_GUI_IDENTIFIER "io.qt.dev.tst-manual-sandboxed-file-access" + XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements" + XCODE_ATTRIBUTE_COPY_PHASE_STRIP FALSE + ) + if(NOT CMAKE_GENERATOR STREQUAL "Xcode") + set_target_properties(tst_manual_sandboxed_file_access PROPERTIES + RESOURCE "${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements" + ) + endif() + + set(platform_plugin "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_PLUGINS}/platforms/libqcocoa.dylib") + target_sources(tst_manual_sandboxed_file_access PRIVATE ${platform_plugin}) + set_source_files_properties(${platform_plugin} + PROPERTIES + MACOSX_PACKAGE_LOCATION PlugIns/platforms + ) + + target_compile_definitions(tst_manual_sandboxed_file_access PRIVATE + QTEST_THROW_ON_FAIL + QTEST_THROW_ON_SKIP + ) +endif() + +if(IOS) + set(plist_path "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.ios") +else() + set(plist_path "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.macos") +endif() + +set_target_properties(tst_manual_sandboxed_file_access + PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${plist_path}") + +install(TARGETS tst_manual_sandboxed_file_access + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +qt_generate_deploy_app_script( + TARGET tst_manual_sandboxed_file_access + OUTPUT_SCRIPT deploy_script + NO_UNSUPPORTED_PLATFORM_ERROR +) +install(SCRIPT ${deploy_script}) diff --git a/tests/manual/sandboxed_file_access/Info.plist.ios b/tests/manual/sandboxed_file_access/Info.plist.ios new file mode 100644 index 00000000000..c6072cffa92 --- /dev/null +++ b/tests/manual/sandboxed_file_access/Info.plist.ios @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>BuildMachineOSBuild</key> + <string>25B78</string> + <key>CFBundleAllowMixedLocalizations</key> + <true/> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleDisplayName</key> + <string>tst_manual_sandboxed_file_access</string> + <key>CFBundleExecutable</key> + <string>tst_manual_sandboxed_file_access</string> + <key>CFBundleIdentifier</key> + <string>io.qt.fb.tst-manual-sandboxed-file-access</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>tst_manual_sandboxed_file_access</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>6.0</string> + <key>CFBundleSupportedPlatforms</key> + <array> + <string>iPhoneOS</string> + </array> + <key>CFBundleVersion</key> + <string>6.0.0</string> + <key>DTCompiler</key> + <string>com.apple.compilers.llvm.clang.1_0</string> + <key>DTPlatformBuild</key> + <string>23B77</string> + <key>DTPlatformName</key> + <string>iphoneos</string> + <key>DTPlatformVersion</key> + <string>26.1</string> + <key>DTSDKBuild</key> + <string>23B77</string> + <key>DTSDKName</key> + <string>iphoneos26.1</string> + <key>DTXcode</key> + <string>2610</string> + <key>DTXcodeBuild</key> + <string>17B55</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>MinimumOSVersion</key> + <string>17</string> + <key>NOTE</key> + <string>This file was generated by Qt's default CMake support.</string> + <key>UIDeviceFamily</key> + <array> + <integer>1</integer> + <integer>2</integer> + </array> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>arm64</string> + </array> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>CFBundleDocumentTypes</key> + <array> + <dict> + <key>CFBundleTypeRole</key> + <string>Editor</string> + <key>LSItemContentTypes</key> + <array> + <string>public.text</string> + </array> + <!-- These two don't seem to be needed to make things work --> + <key>LSHandlerRank</key> + <string>Default</string> + <key>CFBundleTypeName</key> + <string>Text files</string> + </dict> + </array> + <key>UISupportsDocumentBrowser</key> + <true/> +</dict> +</plist> diff --git a/tests/manual/sandboxed_file_access/Info.plist.macos b/tests/manual/sandboxed_file_access/Info.plist.macos new file mode 100644 index 00000000000..81a93f0353f --- /dev/null +++ b/tests/manual/sandboxed_file_access/Info.plist.macos @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>BuildMachineOSBuild</key> + <string>25B78</string> + <key>CFBundleAllowMixedLocalizations</key> + <true/> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>tst_manual_sandboxed_file_access</string> + <key>CFBundleIdentifier</key> + <string>io.qt.dev.tst-manual-sandboxed-file-access</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>tst_manual_sandboxed_file_access</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>6.0</string> + <key>CFBundleSupportedPlatforms</key> + <array> + <string>MacOSX</string> + </array> + <key>CFBundleVersion</key> + <string>6.0.0</string> + <key>DTCompiler</key> + <string>com.apple.compilers.llvm.clang.1_0</string> + <key>DTPlatformBuild</key> + <string>25B74</string> + <key>DTPlatformName</key> + <string>macosx</string> + <key>DTPlatformVersion</key> + <string>26.1</string> + <key>DTSDKBuild</key> + <string>25B74</string> + <key>DTSDKName</key> + <string>macosx26.1</string> + <key>DTXcode</key> + <string>2610</string> + <key>DTXcodeBuild</key> + <string>17B55</string> + <key>LSMinimumSystemVersion</key> + <string>13</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> + <key>CFBundleDocumentTypes</key> + <array> + <dict> + <key>CFBundleTypeRole</key> + <string>Editor</string> + <key>LSItemContentTypes</key> + <array> + <string>public.text</string> + </array> + <!-- These two don't seem to be needed to make things work --> + <key>LSHandlerRank</key> + <string>Default</string> + <key>CFBundleTypeName</key> + <string>Text files</string> + </dict> + </array> +</dict> +</plist> diff --git a/tests/manual/sandboxed_file_access/app.entitlements b/tests/manual/sandboxed_file_access/app.entitlements new file mode 100644 index 00000000000..6d968edb4f8 --- /dev/null +++ b/tests/manual/sandboxed_file_access/app.entitlements @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.app-sandbox</key> + <true/> + <key>com.apple.security.files.user-selected.read-write</key> + <true/> +</dict> +</plist> diff --git a/tests/manual/sandboxed_file_access/tst_sandboxed_file_access.cpp b/tests/manual/sandboxed_file_access/tst_sandboxed_file_access.cpp new file mode 100644 index 00000000000..18381ce0c8c --- /dev/null +++ b/tests/manual/sandboxed_file_access/tst_sandboxed_file_access.cpp @@ -0,0 +1,422 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtCore> +#include <QtWidgets> +#include <QtTest> + +#include <Foundation/Foundation.h> + +#if defined(Q_OS_MACOS) && defined(QT_BUILD_INTERNAL) +#include <private/qcore_mac_p.h> +Q_CONSTRUCTOR_FUNCTION(qt_mac_ensureResponsible); +#endif + +class tst_SandboxedFileAccess : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void cleanupTestCase(); + + void alwaysAccessibleLocations(); + + void standardPaths_data(); + void standardPaths(); + + void readSingleFile(); + void writeSingleFile(); + void writeSingleFileNonCanonical(); + + void removeFile(); + void trashFile(); + + void readFileAfterRestart(); + + void directoryAccess(); + + void securityScopedTargetFile(); + + void fileOpenEvent(); + +private: + void writeFile(const QString &fileName); + QByteArray readFile(const QString &fileName); + + QString getFileName(QFileDialog::AcceptMode, QFileDialog::FileMode, + const QString &action = QString(), const QString &fileName = QString()); + + QString sandboxPath() const + { + return QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first(); + } + + QString bundlePath() const + { + QString path = QCoreApplication::applicationDirPath(); +#if defined(Q_OS_MACOS) + path.remove("/Contents/MacOS"); +#endif + return path; + } + + QStringList m_persistedFileNames; + QPointer<QWidget> m_widget; +}; + +void tst_SandboxedFileAccess::initTestCase() +{ + qDebug() << "đĻ App bundle" << bundlePath(); + qDebug() << "đ App container" << sandboxPath(); + + m_widget = new QWidget; + m_widget->show(); + QVERIFY(QTest::qWaitForWindowExposed(m_widget)); +} + +void tst_SandboxedFileAccess::cleanupTestCase() +{ + NSURL *appSupportDir = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)).toNSURL(); + NSURL *bookmarksFile = [appSupportDir URLByAppendingPathComponent:@"SecurityScopedBookmarks.plist"]; + NSError *error = nullptr; + NSMutableDictionary *bookmarks = [[NSDictionary dictionaryWithContentsOfURL:bookmarksFile + error:&error] mutableCopy]; + for (NSString *path in bookmarks.allKeys) { + if (m_persistedFileNames.contains(QString::fromNSString(path))) { + qDebug() << "Keeping knowledge of persisted path" << path; + continue; + } + qDebug() << "Wiping knowledge of path" << path; + [bookmarks removeObjectForKey:path]; + } + [bookmarks writeToURL:bookmarksFile error:&error]; + + qGuiApp->quit(); +} + +void tst_SandboxedFileAccess::alwaysAccessibleLocations() +{ + readFile(QCoreApplication::applicationFilePath()); + + // The documents location is inside the sandbox and writable on both iOS and macOS + auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + writeFile(documents + "/test-writable-file.txt"); +} + +void tst_SandboxedFileAccess::standardPaths_data() +{ + QTest::addColumn<QStandardPaths::StandardLocation>("location"); + auto standardLocations = QMetaEnum::fromType<QStandardPaths::StandardLocation>(); + for (int i = 0; i < standardLocations.keyCount(); ++i) + QTest::newRow(standardLocations.key(i)) << QStandardPaths::StandardLocation(standardLocations.value(i)); +} + +void tst_SandboxedFileAccess::standardPaths() +{ + QFETCH(QStandardPaths::StandardLocation, location); + auto writableLocation = QStandardPaths::writableLocation(location); + + if (writableLocation.isEmpty()) + QSKIP("There's no writable location for this location"); + + QFileInfo info(writableLocation); + if (info.isSymLink() && !info.symLinkTarget().startsWith(sandboxPath())) + QSKIP("This location is a symlink to outside the sandbox and requires access"); + + QVERIFY(QDir().mkpath(writableLocation)); + +#if !defined(Q_OS_MACOS) + QEXPECT_FAIL("HomeLocation", "The sandbox root is not writable on iOS", Abort); +#endif + writeFile(writableLocation + QString("/test-writable-file-%1.txt").arg(QTest::currentDataTag())); +} + +void tst_SandboxedFileAccess::readSingleFile() +{ + QString filePath = getFileName(QFileDialog::AcceptOpen, + QFileDialog::ExistingFile, "Choose file to read"); + readFile(filePath); + + { + QFile file(QCoreApplication::applicationFilePath()); + QVERIFY(file.open(QFile::ReadOnly)); + QByteArray plistContent = file.read(100); + file.close(); + + // Check that setFileName can target a security scoped file + file.setFileName(filePath); + QVERIFY(file.open(QFile::ReadOnly)); + QVERIFY(file.isReadable()); + QCOMPARE_NE(file.read(100), plistContent); + } + + QDir dir; + QString fileName; + + { + QFileInfo info(filePath); + dir = info.path(); + fileName = info.fileName(); + QVERIFY(dir.exists()); + QVERIFY(!fileName.isEmpty()); + } + + // Check that we're able to access files via non-canonical paths + readFile(dir.absolutePath() + "/../" + dir.dirName() + "/" + fileName); +} + +QByteArray tst_SandboxedFileAccess::readFile(const QString &fileName) +{ + QFile file(fileName); + QVERIFY(file.exists()); + QVERIFY(file.open(QFile::ReadOnly)); + QVERIFY(file.isReadable()); + QByteArray data = file.read(100); + QVERIFY(!data.isEmpty()); + return data; +} + +void tst_SandboxedFileAccess::writeSingleFile() +{ + QString filePath = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile, + "Choose a file to write", "write-single-file.txt"); + writeFile(filePath); + readFile(filePath); +} + +void tst_SandboxedFileAccess::writeSingleFileNonCanonical() +{ + QString filePath = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile, + "Choose a file to write", "write-single-file-non-canonical.txt"); + QDir dir; + QString fileName; + + { + QFileInfo info(filePath); + dir = info.path(); + fileName = info.fileName(); + QVERIFY(dir.exists()); + QVERIFY(!fileName.isEmpty()); + } + + writeFile(dir.absolutePath() + "/../" + dir.dirName() + "/" + fileName); + readFile(filePath); +} + +void tst_SandboxedFileAccess::writeFile(const QString &fileName) +{ + QFile file(fileName); + QVERIFY(file.open(QFile::WriteOnly)); + QVERIFY(file.isWritable()); + QVERIFY(file.write("Hello world")); +} + +void tst_SandboxedFileAccess::removeFile() +{ + QString fileName = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile, + "Choose a file to write and then remove", "write-and-remove-file.txt"); + writeFile(fileName); + + { + QFile file(fileName); + QVERIFY(file.remove()); + } +} + +void tst_SandboxedFileAccess::trashFile() +{ + QString fileName = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile, + "Choose a file to write and then trash", "write-and-trash-file.txt"); + writeFile(fileName); + + { + QFile file(fileName); + QVERIFY(file.moveToTrash()); + } +} + +void tst_SandboxedFileAccess::readFileAfterRestart() +{ + // Every other restart of the app will save a file or load a previously saved file + + QSettings settings; + QString savedFile = settings.value("savedFile").toString(); + if (savedFile.isEmpty()) { + QString filePath = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile, + "Choose a file to write for reading after restart", "write-and-read-after-restart.txt"); + qDebug() << "Writing" << filePath << "and saving to preferences"; + writeFile(filePath); + settings.setValue("savedFile", filePath); + m_persistedFileNames << filePath; + } else { + qDebug() << "Loading" << savedFile << "from preferences"; + settings.remove("savedFile"); // Remove up front, in case this fails + readFile(savedFile); + QFile file(savedFile); + QVERIFY(file.remove()); + } +} + +void tst_SandboxedFileAccess::directoryAccess() +{ + // Every other restart of the app will re-establish access to the folder, + // or re-use previous access. + + QSettings settings; + QString directory = settings.value("savedDirectory").toString(); + if (directory.isEmpty()) { + directory = getFileName(QFileDialog::AcceptOpen, QFileDialog::Directory, + "Choose a directory we can create some files in"); + auto canonical = QFileInfo(directory).canonicalFilePath(); + QVERIFY(!canonical.isEmpty()); + directory = canonical; + settings.setValue("savedDirectory", directory); + m_persistedFileNames << QFileInfo(directory).canonicalFilePath(); + } else { + settings.remove("savedDirectory"); + } + settings.sync(); + + QString fileInDir; + + { + QDir dir(directory); + QVERIFY(dir.exists()); + QVERIFY(dir.isReadable()); + fileInDir = dir.filePath("file-in-dir.txt"); + } + + writeFile(fileInDir); + readFile(fileInDir); + + { + QDir dir(directory); + QVERIFY(dir.count() > 0); + QVERIFY(dir.entryList().contains("file-in-dir.txt")); + } + + { + QDir dir(directory); + QVERIFY(dir.mkdir("subdirectory")); + QVERIFY(dir.entryList().contains("subdirectory")); + fileInDir = dir.filePath("subdirectory/file-in-subdir.txt"); + } + + writeFile(fileInDir); + readFile(fileInDir); + + // Check that we can write to a non-canonical path within the directory + // we have access to, and then read it from the canonical path. + writeFile(directory + "/subdirectory/../non-existing-non-canonical.txt"); + readFile(directory + "/non-existing-non-canonical.txt"); + + { + QDir dir(directory); + QVERIFY(dir.cd("subdirectory")); + dir.removeRecursively(); + } +} + +void tst_SandboxedFileAccess::securityScopedTargetFile() +{ + // This is a non-security scoped file + auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + QString sourceFilePath = documents + "/test-security-scoped-target-file.txt"; + writeFile(sourceFilePath); + QFile sourceFile(sourceFilePath); + + QString directory = getFileName(QFileDialog::AcceptOpen, QFileDialog::Directory, + "Choose a directory we can link/copy some to"); + + QString subDirectory; + { + QDir dir(directory); + QVERIFY(dir.mkdir("subdirectory")); + QVERIFY(dir.entryList().contains("subdirectory")); + subDirectory = dir.filePath("subdirectory"); + } + + QVERIFY(sourceFile.copy(subDirectory + "/copied-file.txt")); + QVERIFY(sourceFile.link(subDirectory + "/linked-file.txt")); + QVERIFY(sourceFile.rename(subDirectory + "/renamed-file.txt")); + + { + QDir dir(directory); + QVERIFY(dir.cd("subdirectory")); + dir.removeRecursively(); + } +} + +void tst_SandboxedFileAccess::fileOpenEvent() +{ + struct OpenEventFilter : public QObject + { + bool eventFilter(QObject *watched, QEvent *event) override + { + if (event->type() == QEvent::FileOpen) { + QFileOpenEvent *openEvent = static_cast<QFileOpenEvent *>(event); + fileName = openEvent->file(); + } + + return QObject::eventFilter(watched, event); + } + + QString fileName; + }; + + OpenEventFilter openEventFilter; + qGuiApp->installEventFilter(&openEventFilter); + + m_widget->setLayout(new QVBoxLayout); + QLabel label; + label.setWordWrap(true); + m_widget->layout()->addWidget(&label); +#if defined(Q_OS_MACOS) + label.setText("Drag a text file to the app's Dock icon, or open in the app via Finder's 'Open With' menu"); +#else + label.setText("Open the Files app, and choose 'Open With' or share a text document with this app"); +#endif + label.show(); + + QTRY_VERIFY_WITH_TIMEOUT(!openEventFilter.fileName.isNull(), 30s); + label.setText("Got file: " + openEventFilter.fileName); + + readFile(openEventFilter.fileName); + + QTest::qWait(3000); +} + +QString tst_SandboxedFileAccess::getFileName(QFileDialog::AcceptMode acceptMode, QFileDialog::FileMode fileMode, + const QString &action, const QString &fileName) +{ + QFileDialog dialog(m_widget); + dialog.setAcceptMode(acceptMode); + dialog.setFileMode(fileMode); + dialog.setWindowTitle(action); + dialog.setLabelText(QFileDialog::Accept, action); + dialog.selectFile(fileName); + if (!action.isEmpty()) + qDebug() << "âšī¸" << action; + dialog.exec(); + auto selectedFiles = dialog.selectedFiles(); + return selectedFiles.count() ? selectedFiles.first() : QString(); +} + +int main(int argc, char** argv) +{ + QApplication app(argc, argv); + + tst_SandboxedFileAccess testObject; + + // Run tests with QApp running + int testExecResult = 0; + QMetaObject::invokeMethod(&testObject, [&]{ + testExecResult = QTest::qExec(&testObject, argc, argv); + }, Qt::QueuedConnection); + + [[maybe_unused]] int appExecResult = app.exec(); + return testExecResult; +} + +#include "tst_sandboxed_file_access.moc" |
