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 <hjk@qt.io>
This commit is contained in:
Marcus Tillmanns
2022-10-13 11:11:29 +02:00
parent 0f24475f48
commit a22d62e57d
8 changed files with 165 additions and 80 deletions

View File

@@ -8,9 +8,20 @@
#include "hostosinfo.h" #include "hostosinfo.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QOperatingSystemVersion>
#include <QRegularExpression> #include <QRegularExpression>
#include <QStorageInfo> #include <QStorageInfo>
#ifdef Q_OS_WIN
#ifdef QTCREATOR_PCH_H
#define CALLBACK WINAPI
#endif
#include <qt_windows.h>
#include <shlobj.h>
#else
#include <qplatformdefs.h>
#endif
namespace Utils { namespace Utils {
// DeviceFileAccess // DeviceFileAccess
@@ -220,6 +231,13 @@ qint64 DeviceFileAccess::bytesAvailable(const FilePath &filePath) const
return -1; return -1;
} }
QByteArray DeviceFileAccess::fileId(const FilePath &filePath) const
{
Q_UNUSED(filePath);
QTC_CHECK(false);
return {};
}
void DeviceFileAccess::asyncFileContents( void DeviceFileAccess::asyncFileContents(
const FilePath &filePath, const FilePath &filePath,
const Continuation<std::optional<QByteArray>> &cont, const Continuation<std::optional<QByteArray>> &cont,
@@ -509,6 +527,74 @@ qint64 DesktopDeviceFileAccess::bytesAvailable(const FilePath &filePath) const
return QStorageInfo(filePath.path()).bytesAvailable(); 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<FILE_INFO_BY_HANDLE_CLASS>(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<const char *>(&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 OsType DesktopDeviceFileAccess::osType(const FilePath &filePath) const
{ {
Q_UNUSED(filePath); Q_UNUSED(filePath);
@@ -714,6 +800,15 @@ qint64 UnixDeviceFileAccess::bytesAvailable(const FilePath &filePath) const
return FileUtils::bytesAvailableFromDFOutput(result.stdOut); 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 FilePathInfo UnixDeviceFileAccess::filePathInfo(const FilePath &filePath) const
{ {
const RunResult stat = runInShell("stat", {"-L", "-c", "%f %Y %s", filePath.path()}); const RunResult stat = runInShell("stat", {"-L", "-c", "%f %Y %s", filePath.path()});

View File

@@ -44,6 +44,7 @@ protected:
virtual bool setPermissions(const FilePath &filePath, QFile::Permissions) const; virtual bool setPermissions(const FilePath &filePath, QFile::Permissions) const;
virtual qint64 fileSize(const FilePath &filePath) const; virtual qint64 fileSize(const FilePath &filePath) const;
virtual qint64 bytesAvailable(const FilePath &filePath) const; virtual qint64 bytesAvailable(const FilePath &filePath) const;
virtual QByteArray fileId(const FilePath &filePath) const;
virtual void iterateDirectory( virtual void iterateDirectory(
const FilePath &filePath, const FilePath &filePath,
@@ -109,6 +110,7 @@ protected:
bool setPermissions(const FilePath &filePath, QFile::Permissions) const override; bool setPermissions(const FilePath &filePath, QFile::Permissions) const override;
qint64 fileSize(const FilePath &filePath) const override; qint64 fileSize(const FilePath &filePath) const override;
qint64 bytesAvailable(const FilePath &filePath) const override; qint64 bytesAvailable(const FilePath &filePath) const override;
QByteArray fileId(const FilePath &filePath) const override;
void iterateDirectory( void iterateDirectory(
const FilePath &filePath, const FilePath &filePath,
@@ -163,6 +165,7 @@ protected:
bool setPermissions(const FilePath &filePath, QFile::Permissions) const override; bool setPermissions(const FilePath &filePath, QFile::Permissions) const override;
qint64 fileSize(const FilePath &filePath) const override; qint64 fileSize(const FilePath &filePath) const override;
qint64 bytesAvailable(const FilePath &filePath) const override; qint64 bytesAvailable(const FilePath &filePath) const override;
QByteArray fileId(const FilePath &filePath) const override;
void iterateDirectory( void iterateDirectory(
const FilePath &filePath, const FilePath &filePath,

View File

@@ -175,13 +175,7 @@ bool Environment::isSameExecutable(const QString &exe1, const QString &exe2) con
for (const QString &i2 : exe2List) { for (const QString &i2 : exe2List) {
const FilePath f1 = FilePath::fromString(i1); const FilePath f1 = FilePath::fromString(i1);
const FilePath f2 = FilePath::fromString(i2); const FilePath f2 = FilePath::fromString(i2);
if (f1 == f2) if (f1.isSameFile(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))
return true; return true;
} }
} }

View File

@@ -515,6 +515,25 @@ bool FilePath::isSameDevice(const FilePath &other) const
return s_deviceHooks.isSameDevice(*this, other); 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 /// \returns an empty FilePath if this is not a symbolic linl
FilePath FilePath::symLinkTarget() const FilePath FilePath::symLinkTarget() const
{ {

View File

@@ -207,6 +207,7 @@ public:
bool needsDevice() const; bool needsDevice() const;
bool isSameDevice(const FilePath &other) const; bool isSameDevice(const FilePath &other) const;
bool isSameFile(const FilePath &other) const;
[[nodiscard]] QFileInfo toFileInfo() const; [[nodiscard]] QFileInfo toFileInfo() const;
[[nodiscard]] static FilePath fromFileInfo(const QFileInfo &info); [[nodiscard]] static FilePath fromFileInfo(const QFileInfo &info);

View File

@@ -5,7 +5,6 @@
#include "savefile.h" #include "savefile.h"
#include "algorithm.h" #include "algorithm.h"
#include "commandline.h"
#include "qtcassert.h" #include "qtcassert.h"
#include "hostosinfo.h" #include "hostosinfo.h"
@@ -15,7 +14,6 @@
#include <QDataStream> #include <QDataStream>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QOperatingSystemVersion>
#include <QRegularExpression> #include <QRegularExpression>
#include <QTemporaryFile> #include <QTemporaryFile>
#include <QTextStream> #include <QTextStream>
@@ -25,7 +23,6 @@
#ifdef QT_GUI_LIB #ifdef QT_GUI_LIB
#include <QMessageBox> #include <QMessageBox>
#include <QRegularExpression>
#include <QGuiApplication> #include <QGuiApplication>
#endif #endif
@@ -37,7 +34,7 @@
#include <shlobj.h> #include <shlobj.h>
#endif #endif
#ifdef Q_OS_OSX #ifdef Q_OS_MACOS
#include "fileutils_mac.h" #include "fileutils_mac.h"
#endif #endif
@@ -337,47 +334,6 @@ FilePaths FileUtils::CopyAskingForOverwrite::files() const
} }
#endif // QT_GUI_LIB #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<FILE_INFO_BY_HANDLE_CLASS>(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<const char *>(&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) FilePath FileUtils::commonPath(const FilePaths &paths)
{ {
if (paths.isEmpty()) if (paths.isEmpty())
@@ -424,33 +380,6 @@ FilePath FileUtils::commonPath(const FilePaths &paths)
return result; 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 #ifdef Q_OS_WIN
template <> template <>
void withNtfsPermissions(const std::function<void()> &task) void withNtfsPermissions(const std::function<void()> &task)

View File

@@ -81,7 +81,6 @@ public:
static bool isAbsolutePath(const QString &fileName) { return !isRelativePath(fileName); } static bool isAbsolutePath(const QString &fileName) { return !isRelativePath(fileName); }
static FilePath commonPath(const FilePath &oldCommonPath, const FilePath &fileName); static FilePath commonPath(const FilePath &oldCommonPath, const FilePath &fileName);
static FilePath commonPath(const FilePaths &paths); static FilePath commonPath(const FilePaths &paths);
static QByteArray fileId(const FilePath &fileName);
static FilePath homePath(); static FilePath homePath();
static FilePaths toFilePathList(const QStringList &paths); static FilePaths toFilePathList(const QStringList &paths);

View File

@@ -101,6 +101,9 @@ private slots:
void cleanPath_data(); void cleanPath_data();
void cleanPath(); void cleanPath();
void isSameFile_data();
void isSameFile();
private: private:
QTemporaryDir tempDir; QTemporaryDir tempDir;
QString rootPath; QString rootPath;
@@ -1114,6 +1117,48 @@ void tst_fileutils::cleanPath()
QCOMPARE(cleaned, expected); QCOMPARE(cleaned, expected);
} }
void tst_fileutils::isSameFile_data()
{
QTest::addColumn<FilePath>("left");
QTest::addColumn<FilePath>("right");
QTest::addColumn<bool>("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) QTEST_GUILESS_MAIN(tst_fileutils)
#include "tst_fileutils.moc" #include "tst_fileutils.moc"