From a22d62e57d900853d135859720920d2ddec4c4f2 Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Thu, 13 Oct 2022 11:11:29 +0200 Subject: [PATCH] Utils: Add FilePath::isSameFile() FilePath::isSameFile() checks if two files are the same file. It first checks if its on the same device. If it is, it will try to read the fileId of the files and compare them. Change-Id: I83668955cacd4e5ed03d43a3fee2be29e9d0a6f0 Reviewed-by: hjk --- src/libs/utils/devicefileaccess.cpp | 95 ++++++++++++++++++++ src/libs/utils/devicefileaccess.h | 3 + src/libs/utils/environment.cpp | 8 +- src/libs/utils/filepath.cpp | 19 ++++ src/libs/utils/filepath.h | 1 + src/libs/utils/fileutils.cpp | 73 +-------------- src/libs/utils/fileutils.h | 1 - tests/auto/utils/fileutils/tst_fileutils.cpp | 45 ++++++++++ 8 files changed, 165 insertions(+), 80 deletions(-) diff --git a/src/libs/utils/devicefileaccess.cpp b/src/libs/utils/devicefileaccess.cpp index 6b07edaf62b..63fc456c8ed 100644 --- a/src/libs/utils/devicefileaccess.cpp +++ b/src/libs/utils/devicefileaccess.cpp @@ -8,9 +8,20 @@ #include "hostosinfo.h" #include +#include #include #include +#ifdef Q_OS_WIN +#ifdef QTCREATOR_PCH_H +#define CALLBACK WINAPI +#endif +#include +#include +#else +#include +#endif + namespace Utils { // DeviceFileAccess @@ -220,6 +231,13 @@ qint64 DeviceFileAccess::bytesAvailable(const FilePath &filePath) const return -1; } +QByteArray DeviceFileAccess::fileId(const FilePath &filePath) const +{ + Q_UNUSED(filePath); + QTC_CHECK(false); + return {}; +} + void DeviceFileAccess::asyncFileContents( const FilePath &filePath, const Continuation> &cont, @@ -509,6 +527,74 @@ qint64 DesktopDeviceFileAccess::bytesAvailable(const FilePath &filePath) const return QStorageInfo(filePath.path()).bytesAvailable(); } +// Copied from qfilesystemengine_win.cpp +#ifdef Q_OS_WIN + +// File ID for Windows up to version 7. +static inline QByteArray fileIdWin7(HANDLE handle) +{ + BY_HANDLE_FILE_INFORMATION info; + if (GetFileInformationByHandle(handle, &info)) { + char buffer[sizeof "01234567:0123456701234567\0"]; + qsnprintf(buffer, sizeof(buffer), "%lx:%08lx%08lx", + info.dwVolumeSerialNumber, + info.nFileIndexHigh, + info.nFileIndexLow); + return QByteArray(buffer); + } + return QByteArray(); +} + +// File ID for Windows starting from version 8. +static QByteArray fileIdWin8(HANDLE handle) +{ + QByteArray result; + FILE_ID_INFO infoEx; + if (GetFileInformationByHandleEx(handle, + static_cast(18), // FileIdInfo in Windows 8 + &infoEx, sizeof(FILE_ID_INFO))) { + result = QByteArray::number(infoEx.VolumeSerialNumber, 16); + result += ':'; + // Note: MinGW-64's definition of FILE_ID_128 differs from the MSVC one. + result += QByteArray(reinterpret_cast(&infoEx.FileId), int(sizeof(infoEx.FileId))).toHex(); + } + return result; +} + +static QByteArray fileIdWin(HANDLE fHandle) +{ + return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8 ? + fileIdWin8(HANDLE(fHandle)) : fileIdWin7(HANDLE(fHandle)); +} +#endif + +QByteArray DesktopDeviceFileAccess::fileId(const FilePath &filePath) const +{ + QByteArray result; + +#ifdef Q_OS_WIN + const HANDLE handle = + CreateFile((wchar_t*)filePath.toUserOutput().utf16(), 0, + FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (handle != INVALID_HANDLE_VALUE) { + result = fileIdWin(handle); + CloseHandle(handle); + } +#else // Copied from qfilesystemengine_unix.cpp + if (Q_UNLIKELY(filePath.isEmpty())) + return result; + + QT_STATBUF statResult; + if (QT_STAT(filePath.toString().toLocal8Bit().constData(), &statResult)) + return result; + result = QByteArray::number(quint64(statResult.st_dev), 16); + result += ':'; + result += QByteArray::number(quint64(statResult.st_ino)); +#endif + return result; +} + OsType DesktopDeviceFileAccess::osType(const FilePath &filePath) const { Q_UNUSED(filePath); @@ -714,6 +800,15 @@ qint64 UnixDeviceFileAccess::bytesAvailable(const FilePath &filePath) const return FileUtils::bytesAvailableFromDFOutput(result.stdOut); } +QByteArray UnixDeviceFileAccess::fileId(const FilePath &filePath) const +{ + const RunResult result = runInShell("stat", {"-L", "-c", "%D:%i", filePath.path()}); + if (result.exitCode != 0) + return {}; + + return result.stdOut; +} + FilePathInfo UnixDeviceFileAccess::filePathInfo(const FilePath &filePath) const { const RunResult stat = runInShell("stat", {"-L", "-c", "%f %Y %s", filePath.path()}); diff --git a/src/libs/utils/devicefileaccess.h b/src/libs/utils/devicefileaccess.h index e1ef5db1ddb..f638d162606 100644 --- a/src/libs/utils/devicefileaccess.h +++ b/src/libs/utils/devicefileaccess.h @@ -44,6 +44,7 @@ protected: virtual bool setPermissions(const FilePath &filePath, QFile::Permissions) const; virtual qint64 fileSize(const FilePath &filePath) const; virtual qint64 bytesAvailable(const FilePath &filePath) const; + virtual QByteArray fileId(const FilePath &filePath) const; virtual void iterateDirectory( const FilePath &filePath, @@ -109,6 +110,7 @@ protected: bool setPermissions(const FilePath &filePath, QFile::Permissions) const override; qint64 fileSize(const FilePath &filePath) const override; qint64 bytesAvailable(const FilePath &filePath) const override; + QByteArray fileId(const FilePath &filePath) const override; void iterateDirectory( const FilePath &filePath, @@ -163,6 +165,7 @@ protected: bool setPermissions(const FilePath &filePath, QFile::Permissions) const override; qint64 fileSize(const FilePath &filePath) const override; qint64 bytesAvailable(const FilePath &filePath) const override; + QByteArray fileId(const FilePath &filePath) const override; void iterateDirectory( const FilePath &filePath, diff --git a/src/libs/utils/environment.cpp b/src/libs/utils/environment.cpp index 57b3b197244..62dc52b2605 100644 --- a/src/libs/utils/environment.cpp +++ b/src/libs/utils/environment.cpp @@ -175,13 +175,7 @@ bool Environment::isSameExecutable(const QString &exe1, const QString &exe2) con for (const QString &i2 : exe2List) { const FilePath f1 = FilePath::fromString(i1); const FilePath f2 = FilePath::fromString(i2); - if (f1 == f2) - return true; - if (f1.needsDevice() != f2.needsDevice() || f1.scheme() != f2.scheme()) - return false; - if (f1.resolveSymlinks() == f2.resolveSymlinks()) - return true; - if (FileUtils::fileId(f1) == FileUtils::fileId(f2)) + if (f1.isSameFile(f2)) return true; } } diff --git a/src/libs/utils/filepath.cpp b/src/libs/utils/filepath.cpp index 645ded358e5..057ed05847b 100644 --- a/src/libs/utils/filepath.cpp +++ b/src/libs/utils/filepath.cpp @@ -515,6 +515,25 @@ bool FilePath::isSameDevice(const FilePath &other) const return s_deviceHooks.isSameDevice(*this, other); } +bool FilePath::isSameFile(const FilePath &other) const +{ + if (*this == other) + return true; + + if (!isSameDevice(other)) + return false; + + const QByteArray fileId = fileAccess()->fileId(*this); + const QByteArray otherFileId = fileAccess()->fileId(other); + if (fileId.isEmpty() || otherFileId.isEmpty()) + return false; + + if (fileId == otherFileId) + return true; + + return false; +} + /// \returns an empty FilePath if this is not a symbolic linl FilePath FilePath::symLinkTarget() const { diff --git a/src/libs/utils/filepath.h b/src/libs/utils/filepath.h index 0cc9455eef7..97e5752ccf8 100644 --- a/src/libs/utils/filepath.h +++ b/src/libs/utils/filepath.h @@ -207,6 +207,7 @@ public: bool needsDevice() const; bool isSameDevice(const FilePath &other) const; + bool isSameFile(const FilePath &other) const; [[nodiscard]] QFileInfo toFileInfo() const; [[nodiscard]] static FilePath fromFileInfo(const QFileInfo &info); diff --git a/src/libs/utils/fileutils.cpp b/src/libs/utils/fileutils.cpp index 989f6a3fcc9..ce5ac3a2497 100644 --- a/src/libs/utils/fileutils.cpp +++ b/src/libs/utils/fileutils.cpp @@ -5,7 +5,6 @@ #include "savefile.h" #include "algorithm.h" -#include "commandline.h" #include "qtcassert.h" #include "hostosinfo.h" @@ -15,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -25,7 +23,6 @@ #ifdef QT_GUI_LIB #include -#include #include #endif @@ -37,7 +34,7 @@ #include #endif -#ifdef Q_OS_OSX +#ifdef Q_OS_MACOS #include "fileutils_mac.h" #endif @@ -337,47 +334,6 @@ FilePaths FileUtils::CopyAskingForOverwrite::files() const } #endif // QT_GUI_LIB -// Copied from qfilesystemengine_win.cpp -#ifdef Q_OS_WIN - -// File ID for Windows up to version 7. -static inline QByteArray fileIdWin7(HANDLE handle) -{ - BY_HANDLE_FILE_INFORMATION info; - if (GetFileInformationByHandle(handle, &info)) { - char buffer[sizeof "01234567:0123456701234567\0"]; - qsnprintf(buffer, sizeof(buffer), "%lx:%08lx%08lx", - info.dwVolumeSerialNumber, - info.nFileIndexHigh, - info.nFileIndexLow); - return QByteArray(buffer); - } - return QByteArray(); -} - -// File ID for Windows starting from version 8. -static QByteArray fileIdWin8(HANDLE handle) -{ - QByteArray result; - FILE_ID_INFO infoEx; - if (GetFileInformationByHandleEx(handle, - static_cast(18), // FileIdInfo in Windows 8 - &infoEx, sizeof(FILE_ID_INFO))) { - result = QByteArray::number(infoEx.VolumeSerialNumber, 16); - result += ':'; - // Note: MinGW-64's definition of FILE_ID_128 differs from the MSVC one. - result += QByteArray(reinterpret_cast(&infoEx.FileId), int(sizeof(infoEx.FileId))).toHex(); - } - return result; -} - -static QByteArray fileIdWin(HANDLE fHandle) -{ - return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8 ? - fileIdWin8(HANDLE(fHandle)) : fileIdWin7(HANDLE(fHandle)); -} -#endif - FilePath FileUtils::commonPath(const FilePaths &paths) { if (paths.isEmpty()) @@ -424,33 +380,6 @@ FilePath FileUtils::commonPath(const FilePaths &paths) return result; } -QByteArray FileUtils::fileId(const FilePath &fileName) -{ - QByteArray result; - -#ifdef Q_OS_WIN - const HANDLE handle = - CreateFile((wchar_t*)fileName.toUserOutput().utf16(), 0, - FILE_SHARE_READ, NULL, OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS, NULL); - if (handle != INVALID_HANDLE_VALUE) { - result = fileIdWin(handle); - CloseHandle(handle); - } -#else // Copied from qfilesystemengine_unix.cpp - if (Q_UNLIKELY(fileName.isEmpty())) - return result; - - QT_STATBUF statResult; - if (QT_STAT(fileName.toString().toLocal8Bit().constData(), &statResult)) - return result; - result = QByteArray::number(quint64(statResult.st_dev), 16); - result += ':'; - result += QByteArray::number(quint64(statResult.st_ino)); -#endif - return result; -} - #ifdef Q_OS_WIN template <> void withNtfsPermissions(const std::function &task) diff --git a/src/libs/utils/fileutils.h b/src/libs/utils/fileutils.h index 76ad91efbc8..bbeb906ee34 100644 --- a/src/libs/utils/fileutils.h +++ b/src/libs/utils/fileutils.h @@ -81,7 +81,6 @@ public: static bool isAbsolutePath(const QString &fileName) { return !isRelativePath(fileName); } static FilePath commonPath(const FilePath &oldCommonPath, const FilePath &fileName); static FilePath commonPath(const FilePaths &paths); - static QByteArray fileId(const FilePath &fileName); static FilePath homePath(); static FilePaths toFilePathList(const QStringList &paths); diff --git a/tests/auto/utils/fileutils/tst_fileutils.cpp b/tests/auto/utils/fileutils/tst_fileutils.cpp index 0248fb44891..4d600a68c2f 100644 --- a/tests/auto/utils/fileutils/tst_fileutils.cpp +++ b/tests/auto/utils/fileutils/tst_fileutils.cpp @@ -101,6 +101,9 @@ private slots: void cleanPath_data(); void cleanPath(); + void isSameFile_data(); + void isSameFile(); + private: QTemporaryDir tempDir; QString rootPath; @@ -1114,6 +1117,48 @@ void tst_fileutils::cleanPath() QCOMPARE(cleaned, expected); } +void tst_fileutils::isSameFile_data() +{ + QTest::addColumn("left"); + QTest::addColumn("right"); + QTest::addColumn("shouldBeEqual"); + + QTest::addRow("/==/") + << FilePath::fromString("/") << FilePath::fromString("/") << true; + QTest::addRow("/!=tmp") + << FilePath::fromString("/") << FilePath::fromString(tempDir.path()) << false; + + + QDir dir(tempDir.path()); + touch(dir, "target-file", false); + + QFile file(dir.absoluteFilePath("target-file")); + if (file.link(dir.absoluteFilePath("source-file"))) { + QTest::addRow("real==link") + << FilePath::fromString(file.fileName()) + << FilePath::fromString(dir.absoluteFilePath("target-file")) + << true; + } + + QTest::addRow("/!=non-existing") + << FilePath::fromString("/") << FilePath::fromString("/this-path/does-not-exist") << false; + + QTest::addRow("two-devices") << FilePath::fromString( + "docker://boot2qt-raspberrypi4-64:6.5.0/opt/toolchain/sysroots/aarch64-pokysdk-linux/usr/" + "bin/aarch64-poky-linux/aarch64-poky-linux-g++") + << FilePath::fromString("docker://qt-linux:6/usr/bin/g++") + << false; +} + +void tst_fileutils::isSameFile() +{ + QFETCH(FilePath, left); + QFETCH(FilePath, right); + QFETCH(bool, shouldBeEqual); + + QCOMPARE(left.isSameFile(right), shouldBeEqual); +} + QTEST_GUILESS_MAIN(tst_fileutils) #include "tst_fileutils.moc"