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.

Pick-to: 6.8
Task-number: QTBUG-28246
Task-number: QTBUG-31103
Change-Id: Ibdb902df3f169b016a519f67ad5a79e6afb6aae3
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Tor Arne Vestbø 2024-09-19 22:29:01 +02:00
parent 7d6bac5f2b
commit 3d08816f4c
15 changed files with 178 additions and 28 deletions

View File

@ -27,6 +27,8 @@
# include "qmutex.h"
#endif
#include <private/qorderedmutexlocker_p.h>
#include <algorithm>
#include <memory>
#include <stdlib.h>
@ -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;

View File

@ -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;

View File

@ -23,6 +23,7 @@ class Q_CORE_EXPORT QFileInfo
{
friend class QDirIteratorPrivate;
friend class QDirListingPrivate;
friend class QFileInfoPrivate;
public:
explicit QFileInfo(QFileInfoPrivate *d);

View File

@ -45,6 +45,8 @@ public:
CachedPerms = 0x100
};
static QFileInfoPrivate *get(QFileInfo *fi) { return fi->d_func(); }
inline QFileInfoPrivate()
: QSharedData(), fileEngine(nullptr),
cachedFlags(0),

View File

@ -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,

View File

@ -874,7 +874,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;
}
@ -1029,6 +1029,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
@ -1838,4 +1845,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

View File

@ -1861,6 +1861,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)

View File

@ -111,6 +111,8 @@ public:
UserId = 0x10000000,
GroupId = 0x20000000,
CaseSensitive = 0x80000000,
OwnerIds = UserId | GroupId,
PosixStatFlags = QFileSystemMetaData::OtherPermissions

View File

@ -863,10 +863,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.
@ -1040,6 +1036,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
*/

View File

@ -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();

View File

@ -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;

View File

@ -29,6 +29,7 @@
#include <qdir.h>
#include <qelapsedtimer.h>
#include <private/qfileinfo_p.h>
#include <private/qfilesystemengine_p.h>
#include <utility>
@ -57,7 +58,8 @@ public:
#ifndef QT_NO_FSFILEENGINE
bool isCaseSensitive() const {
return QFileSystemEngine::isCaseSensitive();
auto *fiPriv = QFileInfoPrivate::get(const_cast<QFileInfo*>(&mFileInfo));
return QFileSystemEngine::isCaseSensitive(fiPriv->fileEntry, fiPriv->metaData);
}
#endif

View File

@ -38,6 +38,10 @@
#include <Foundation/Foundation.h>
#endif
#if QT_CONFIG(process)
#include <QProcess>
#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<QString>("fileSystemType");
QTest::addColumn<QString>("volumeName");
QTest::addColumn<Qt::CaseSensitivity>("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"

View File

@ -24,6 +24,7 @@
#if defined(Q_OS_WIN)
# include <qt_windows.h> // for SetFileAttributes
#endif
#include <private/qfileinfo_p.h>
#include <private/qfilesystemengine_p.h>
#include <algorithm>
@ -1055,11 +1056,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);
}
}
}

View File

@ -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<bool>("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"