From 42439087eb6b99022f852588b77653809f2d1b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Thu, 19 Sep 2024 22:29:01 +0200 Subject: [PATCH] Darwin: Teach QFileSystemEngine how to resolve case-sensitivity Both APFS and HFS+ can be both case-sensitive and case-insensitive (the default), and the mounted file system may be any other file system than these two as well, so hard-coding to case-sensitive is not sufficient. Task-number: QTBUG-28246 Task-number: QTBUG-31103 Change-Id: Ibdb902df3f169b016a519f67ad5a79e6afb6aae3 Reviewed-by: Thiago Macieira (cherry picked from commit 3d08816f4c4245f08a53307775fe3c4ed31a7a32) Reviewed-by: Qt Cherry-pick Bot --- src/corelib/io/qdir.cpp | 9 ++- src/corelib/io/qfileinfo.cpp | 6 +- src/corelib/io/qfileinfo.h | 1 + src/corelib/io/qfileinfo_p.h | 2 + src/corelib/io/qfilesystemengine_p.h | 10 +-- src/corelib/io/qfilesystemengine_unix.cpp | 24 ++++++- src/corelib/io/qfilesystemengine_win.cpp | 6 ++ src/corelib/io/qfilesystemmetadata_p.h | 2 + src/corelib/io/qfsfileengine.cpp | 14 ++-- src/corelib/io/qfsfileengine_unix.cpp | 5 -- src/corelib/io/qfsfileengine_win.cpp | 5 -- src/gui/itemmodels/qfileinfogatherer_p.h | 4 +- .../corelib/io/qfileinfo/tst_qfileinfo.cpp | 72 +++++++++++++++++++ .../qfilesystemmodel/tst_qfilesystemmodel.cpp | 14 +++- .../io/qfileinfo/tst_bench_qfileinfo.cpp | 32 +++++++++ 15 files changed, 178 insertions(+), 28 deletions(-) diff --git a/src/corelib/io/qdir.cpp b/src/corelib/io/qdir.cpp index 80592cb64d7..4e0ef6d9744 100644 --- a/src/corelib/io/qdir.cpp +++ b/src/corelib/io/qdir.cpp @@ -27,6 +27,8 @@ # include "qmutex.h" #endif +#include + #include #include #include @@ -1828,7 +1830,12 @@ bool comparesEqual(const QDir &lhs, const QDir &rhs) if (d->fileEngine.get() != other->fileEngine.get()) // one is native, the other is a custom file-engine return false; - sensitive = QFileSystemEngine::isCaseSensitive() ? Qt::CaseSensitive : Qt::CaseInsensitive; + QOrderedMutexLocker locker(&d->fileCache.mutex, &other->fileCache.mutex); + const bool thisCaseSensitive = QFileSystemEngine::isCaseSensitive(d->dirEntry, d->fileCache.metaData); + if (thisCaseSensitive != QFileSystemEngine::isCaseSensitive(other->dirEntry, other->fileCache.metaData)) + return false; + + sensitive = thisCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; } else { if (d->fileEngine->caseSensitive() != other->fileEngine->caseSensitive()) return false; diff --git a/src/corelib/io/qfileinfo.cpp b/src/corelib/io/qfileinfo.cpp index 6bc0128affe..f38c8f67617 100644 --- a/src/corelib/io/qfileinfo.cpp +++ b/src/corelib/io/qfileinfo.cpp @@ -463,7 +463,11 @@ bool comparesEqual(const QFileInfo &lhs, const QFileInfo &rhs) if (lhs.d_ptr->fileEngine != rhs.d_ptr->fileEngine) // one is native, the other is a custom file-engine return false; - sensitive = QFileSystemEngine::isCaseSensitive() ? Qt::CaseSensitive : Qt::CaseInsensitive; + const bool lhsCaseSensitive = QFileSystemEngine::isCaseSensitive(lhs.d_ptr->fileEntry, lhs.d_ptr->metaData); + if (lhsCaseSensitive != QFileSystemEngine::isCaseSensitive(rhs.d_ptr->fileEntry, rhs.d_ptr->metaData)) + return false; + + sensitive = lhsCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; } else { if (lhs.d_ptr->fileEngine->caseSensitive() != rhs.d_ptr->fileEngine->caseSensitive()) return false; diff --git a/src/corelib/io/qfileinfo.h b/src/corelib/io/qfileinfo.h index 7336a4e9e7c..732182049fb 100644 --- a/src/corelib/io/qfileinfo.h +++ b/src/corelib/io/qfileinfo.h @@ -23,6 +23,7 @@ class Q_CORE_EXPORT QFileInfo { friend class QDirIteratorPrivate; friend class QDirListingPrivate; + friend class QFileInfoPrivate; public: explicit QFileInfo(QFileInfoPrivate *d); diff --git a/src/corelib/io/qfileinfo_p.h b/src/corelib/io/qfileinfo_p.h index 4091a7464a8..666d0edc338 100644 --- a/src/corelib/io/qfileinfo_p.h +++ b/src/corelib/io/qfileinfo_p.h @@ -45,6 +45,8 @@ class QFileInfoPrivate : public QSharedData CachedPerms = 0x100 }; + static QFileInfoPrivate *get(QFileInfo *fi) { return fi->d_func(); } + inline QFileInfoPrivate() : QSharedData(), fileEngine(nullptr), cachedFlags(0), diff --git a/src/corelib/io/qfilesystemengine_p.h b/src/corelib/io/qfilesystemengine_p.h index 814915407ea..48d4519d688 100644 --- a/src/corelib/io/qfilesystemengine_p.h +++ b/src/corelib/io/qfilesystemengine_p.h @@ -58,14 +58,10 @@ inline bool qIsFilenameBroken(const QFileSystemEntry &entry) class Q_AUTOTEST_EXPORT QFileSystemEngine { public: - static bool isCaseSensitive() - { -#ifndef Q_OS_WIN - return true; -#else - return false; +#ifndef QT_BUILD_INTERNAL + Q_CORE_EXPORT #endif - } + static bool isCaseSensitive(const QFileSystemEntry &entry, QFileSystemMetaData &data); static QFileSystemEntry getLinkTarget(const QFileSystemEntry &link, QFileSystemMetaData &data); static QFileSystemEntry getRawLinkPath(const QFileSystemEntry &link, diff --git a/src/corelib/io/qfilesystemengine_unix.cpp b/src/corelib/io/qfilesystemengine_unix.cpp index 14beb89b26a..00b42dd0cf2 100644 --- a/src/corelib/io/qfilesystemengine_unix.cpp +++ b/src/corelib/io/qfilesystemengine_unix.cpp @@ -840,7 +840,7 @@ bool QFileSystemEngine::fillMetaData(const QFileSystemEntry &entry, QFileSystemM Q_CHECK_FILE_NAME(entry, false); #if defined(Q_OS_DARWIN) - if (what & QFileSystemMetaData::BundleType) { + if (what & (QFileSystemMetaData::BundleType | QFileSystemMetaData::CaseSensitive)) { if (!data.hasFlags(QFileSystemMetaData::DirectoryType)) what |= QFileSystemMetaData::DirectoryType; } @@ -995,6 +995,13 @@ bool QFileSystemEngine::fillMetaData(const QFileSystemEntry &entry, QFileSystemM data.knownFlagsMask |= QFileSystemMetaData::BundleType; } + + if (what & QFileSystemMetaData::CaseSensitive) { + if (entryErrno == 0 && hasResourcePropertyFlag( + data, entry, kCFURLVolumeSupportsCaseSensitiveNamesKey)) + data.entryFlags |= QFileSystemMetaData::CaseSensitive; + data.knownFlagsMask |= QFileSystemMetaData::CaseSensitive; + } #endif if (what & QFileSystemMetaData::HiddenAttribute @@ -1788,4 +1795,19 @@ QFileSystemEntry QFileSystemEngine::currentPath() #endif return result; } + +bool QFileSystemEngine::isCaseSensitive(const QFileSystemEntry &entry, QFileSystemMetaData &metaData) +{ +#if defined(Q_OS_DARWIN) + if (!metaData.hasFlags(QFileSystemMetaData::CaseSensitive)) + fillMetaData(entry, metaData, QFileSystemMetaData::CaseSensitive); + return metaData.entryFlags.testFlag(QFileSystemMetaData::CaseSensitive); +#else + Q_UNUSED(entry); + Q_UNUSED(metaData); + // FIXME: This may not be accurate for all file systems (QTBUG-28246) + return true; +#endif +} + QT_END_NAMESPACE diff --git a/src/corelib/io/qfilesystemengine_win.cpp b/src/corelib/io/qfilesystemengine_win.cpp index 89054a95bdf..8300c3b5eea 100644 --- a/src/corelib/io/qfilesystemengine_win.cpp +++ b/src/corelib/io/qfilesystemengine_win.cpp @@ -1855,6 +1855,12 @@ bool QFileSystemEngine::setPermissions(const QFileSystemEntry &entry, return ret; } +bool QFileSystemEngine::isCaseSensitive(const QFileSystemEntry &, QFileSystemMetaData &) +{ + // FIXME: This may not be accurate for all file systems (QTBUG-28246) + return false; +} + static inline QDateTime fileTimeToQDateTime(const FILETIME *time) { if (time->dwHighDateTime == 0 && time->dwLowDateTime == 0) diff --git a/src/corelib/io/qfilesystemmetadata_p.h b/src/corelib/io/qfilesystemmetadata_p.h index 12aebdd7079..ded45734d59 100644 --- a/src/corelib/io/qfilesystemmetadata_p.h +++ b/src/corelib/io/qfilesystemmetadata_p.h @@ -111,6 +111,8 @@ class Q_AUTOTEST_EXPORT QFileSystemMetaData UserId = 0x10000000, GroupId = 0x20000000, + CaseSensitive = 0x80000000, + OwnerIds = UserId | GroupId, PosixStatFlags = QFileSystemMetaData::OtherPermissions diff --git a/src/corelib/io/qfsfileengine.cpp b/src/corelib/io/qfsfileengine.cpp index 2f9087bf6a3..26f52b8b7ba 100644 --- a/src/corelib/io/qfsfileengine.cpp +++ b/src/corelib/io/qfsfileengine.cpp @@ -869,10 +869,6 @@ bool QFSFileEngine::supportsExtension(Extension extension) const return false; } -/*! \fn bool QFSFileEngine::caseSensitive() const - Returns \c false for Windows, true for Unix. -*/ - /*! \fn QString QFSFileEngine::currentPath(const QString &fileName) For Unix, returns the current working directory for the file engine. @@ -1046,6 +1042,16 @@ bool QFSFileEngine::setCurrentPath(const QString &path) return QFileSystemEngine::setCurrentPath(QFileSystemEntry(path)); } +/*! + Returns whether the file system considers the file name to be + case sensitive. +*/ +bool QFSFileEngine::caseSensitive() const +{ + Q_D(const QFSFileEngine); + return QFileSystemEngine::isCaseSensitive(d->fileEntry, d->metaData); +} + /*! \fn bool QFSFileEngine::setPermissions(uint perms) \reimp */ diff --git a/src/corelib/io/qfsfileengine_unix.cpp b/src/corelib/io/qfsfileengine_unix.cpp index 5806689182f..afcbb104413 100644 --- a/src/corelib/io/qfsfileengine_unix.cpp +++ b/src/corelib/io/qfsfileengine_unix.cpp @@ -294,11 +294,6 @@ qint64 QFSFileEnginePrivate::nativeSize() const return sizeFdFh(); } -bool QFSFileEngine::caseSensitive() const -{ - return true; -} - QString QFSFileEngine::currentPath(const QString &) { return QFileSystemEngine::currentPath().filePath(); diff --git a/src/corelib/io/qfsfileengine_win.cpp b/src/corelib/io/qfsfileengine_win.cpp index 4ac305f49b6..bf8bb102d81 100644 --- a/src/corelib/io/qfsfileengine_win.cpp +++ b/src/corelib/io/qfsfileengine_win.cpp @@ -425,11 +425,6 @@ bool QFSFileEnginePrivate::nativeRenameOverwrite(const QFileSystemEntry &newEntr return res; } -bool QFSFileEngine::caseSensitive() const -{ - return false; -} - QString QFSFileEngine::currentPath(const QString &fileName) { QString ret; diff --git a/src/gui/itemmodels/qfileinfogatherer_p.h b/src/gui/itemmodels/qfileinfogatherer_p.h index 3d5f59c22ed..6ff54664a81 100644 --- a/src/gui/itemmodels/qfileinfogatherer_p.h +++ b/src/gui/itemmodels/qfileinfogatherer_p.h @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -57,7 +58,8 @@ class QExtendedInformation { #ifndef QT_NO_FSFILEENGINE bool isCaseSensitive() const { - return QFileSystemEngine::isCaseSensitive(); + auto *fiPriv = QFileInfoPrivate::get(const_cast(&mFileInfo)); + return QFileSystemEngine::isCaseSensitive(fiPriv->fileEntry, fiPriv->metaData); } #endif diff --git a/tests/auto/corelib/io/qfileinfo/tst_qfileinfo.cpp b/tests/auto/corelib/io/qfileinfo/tst_qfileinfo.cpp index e0cd361fdb4..644741f16e0 100644 --- a/tests/auto/corelib/io/qfileinfo/tst_qfileinfo.cpp +++ b/tests/auto/corelib/io/qfileinfo/tst_qfileinfo.cpp @@ -38,6 +38,10 @@ #include #endif +#if QT_CONFIG(process) +#include +#endif + #if defined(Q_OS_VXWORKS) #define Q_NO_SYMLINKS #endif @@ -242,6 +246,11 @@ private slots: void stdfilesystem(); void readSymLink(); +#if defined(Q_OS_DARWIN) + void fileSystemCaseSensitivity_data(); + void fileSystemCaseSensitivity(); +#endif + private: const QString m_currentDir; QString m_sourceFile; @@ -2448,5 +2457,68 @@ void tst_QFileInfo::readSymLink() QFileInfo info(symLinkName); QCOMPARE(info.readSymLink(), QString("../../a")); } + +#if defined(Q_OS_DARWIN) +void tst_QFileInfo::fileSystemCaseSensitivity_data() +{ + QTest::addColumn("fileSystemType"); + QTest::addColumn("volumeName"); + QTest::addColumn("caseSensitivity"); + + QTest::newRow("APFS") << "APFS" << "apfs" << Qt::CaseInsensitive; + QTest::newRow("APFS case-sensitive") << "Case-sensitive APFS" << "apfs_case_sensitive" << Qt::CaseSensitive; + QTest::newRow("HFS+") << "HFS+" << "hfs" << Qt::CaseInsensitive; + QTest::newRow("HFS+ case-sensitive") << "Case-sensitive HFS+" << "hfs_case_sensitive" << Qt::CaseSensitive; + QTest::newRow("FAT32") << "MS-DOS FAT32" << "fat32" << Qt::CaseInsensitive; + QTest::newRow("ExFAT") << "ExFAT" << "exfat" << Qt::CaseInsensitive; +} + +void tst_QFileInfo::fileSystemCaseSensitivity() +{ +#if !QT_CONFIG(process) + QSKIP("No QProcess available"); +#else + QTemporaryDir tmpDir; + QVERIFY2(tmpDir.isValid(), qPrintable(tmpDir.errorString())); + + QFETCH(QString, fileSystemType); + QFETCH(QString, volumeName); + + QString imageName = tmpDir.filePath(QString("%2.sparseimage").arg(volumeName)); + + QVERIFY(QProcess::execute("hdiutil", { "create", "-quiet", + "-type", "SPARSE", "-size", "50m", "-fs", fileSystemType, + "-volname", volumeName, imageName }) == 0); + + QVERIFY(QProcess::execute("hdiutil", { "attach", "-quiet", + "-mountroot", tmpDir.path(), imageName }) == 0); + + QDir mountPoint(tmpDir.filePath(volumeName)); + QVERIFY(mountPoint.exists()); + + auto cleanup = qScopeGuard([&] { + QVERIFY(QProcess::execute("hdiutil", { "detach", + "-quiet", mountPoint.absolutePath() }) == 0); + }); + + QFileInfo lowerCase(mountPoint.filePath("foo")); + { + QFile file(lowerCase.filePath()); + QVERIFY(file.open(QFile::WriteOnly)); + } + + QFileInfo upperCase(mountPoint.filePath("FOO")); + { + QFile file(upperCase.filePath()); + QVERIFY(file.open(QFile::WriteOnly)); + } + + QFETCH(Qt::CaseSensitivity, caseSensitivity); + QCOMPARE(lowerCase == upperCase, caseSensitivity == Qt::CaseInsensitive); + +#endif // QT_CONFIG(process) +} +#endif // defined(Q_OS_DARWIN) + QTEST_MAIN(tst_QFileInfo) #include "tst_qfileinfo.moc" diff --git a/tests/auto/gui/itemmodels/qfilesystemmodel/tst_qfilesystemmodel.cpp b/tests/auto/gui/itemmodels/qfilesystemmodel/tst_qfilesystemmodel.cpp index bd4d778d112..d1e2e4393eb 100644 --- a/tests/auto/gui/itemmodels/qfilesystemmodel/tst_qfilesystemmodel.cpp +++ b/tests/auto/gui/itemmodels/qfilesystemmodel/tst_qfilesystemmodel.cpp @@ -24,6 +24,7 @@ #if defined(Q_OS_WIN) # include // for SetFileAttributes #endif +#include #include #include @@ -1052,11 +1053,18 @@ void tst_QFileSystemModel::caseSensitivity() indexes.append(index); } - if (!QFileSystemEngine::isCaseSensitive()) { - // QTBUG-31103, QTBUG-64147: Verify that files can be accessed by paths with fLipPeD case. + QFileInfo tmpInfo(tmp); + auto *tmpInfoPriv = QFileInfoPrivate::get(&tmpInfo); + if (!QFileSystemEngine::isCaseSensitive(tmpInfoPriv->fileEntry, tmpInfoPriv->metaData)) { + // Verify that files can be accessed by paths with fLipPeD case. for (int i = 0; i < paths.size(); ++i) { + const QModelIndex normalCaseIndex = indexes.at(i); const QModelIndex flippedCaseIndex = model->index(flipCase(paths.at(i))); - QCOMPARE(indexes.at(i), flippedCaseIndex); +#if !defined(Q_OS_WIN) + QEXPECT_FAIL("", "QFileSystemModelNodePathKey is hard-coded to be case" + " sensitive on non-Windows and case-insensitive on Windows (QTBUG-31103)", Abort); +#endif + QCOMPARE(normalCaseIndex, flippedCaseIndex); } } } diff --git a/tests/benchmarks/corelib/io/qfileinfo/tst_bench_qfileinfo.cpp b/tests/benchmarks/corelib/io/qfileinfo/tst_bench_qfileinfo.cpp index 2c626dde70b..0ae19c26af0 100644 --- a/tests/benchmarks/corelib/io/qfileinfo/tst_bench_qfileinfo.cpp +++ b/tests/benchmarks/corelib/io/qfileinfo/tst_bench_qfileinfo.cpp @@ -20,6 +20,8 @@ private slots: void symLinkTargetPerformanceLNK(); void junctionTargetPerformanceMountpoint(); #endif + void comparison_data(); + void comparison(); }; void tst_QFileInfo::existsTemporary() @@ -72,6 +74,36 @@ void tst_QFileInfo::junctionTargetPerformanceMountpoint() } #endif +void tst_QFileInfo::comparison_data() +{ + QTest::addColumn("shouldExist"); + QTest::addRow("files do not exist") << false; + QTest::addRow("files exists") << true; +} + +void tst_QFileInfo::comparison() +{ + QTemporaryDir tmpDir; + QVERIFY2(tmpDir.isValid(), qPrintable(tmpDir.errorString())); + + QFETCH(bool, shouldExist); + + QFileInfo firstInfo(tmpDir.filePath("first")); + QFileInfo secondInfo(tmpDir.filePath("second")); + + if (shouldExist) { + QFile first(firstInfo.absoluteFilePath()); + QVERIFY(first.open(QFile::WriteOnly)); + QFile second(secondInfo.absoluteFilePath()); + QVERIFY(second.open(QFile::WriteOnly)); + } + + QBENCHMARK { + // Comparison should be fast because we cache file info + [[maybe_unused]] auto r = firstInfo == secondInfo; + } +} + QTEST_MAIN(tst_QFileInfo) #include "tst_bench_qfileinfo.moc"