Make Qt aware of symlinks and shortcuts on Windows

Qt has traditionally considered Windows shortcut files equivalent to
symlinks on Unix file systems. Because of NTFS symlinks, the
interpretation of shotcut files as symlinks is confusing.

In this change, QFileInfo treats shortcut (.lnk) files as regular files
but can follow the pointed object.

In addition, QFileInfo introduces a more comprehensive file type. So
that applications can make well-informed decisions about how to treat a
file system entry.

Based on the implementation of QFileInfo::type(), two inline helper
functions are introduced to QFileInfo.
1. isSymbolicLink, returns true if it points to a symbolic link.
2. isShortcut, returns true if it points to a shortcut.

[ChangeLog][QtCore][QFileInfo] Introduce QFileInfo::type() to replace
the isSymLink method.

Task-number: QTBUG-75869
Change-Id: Icc0dd52f9ad0ea50b0265d77ee0d0a3d25054e39
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
bb10
Ryan Chu 2019-07-11 13:23:59 +02:00
parent 7ac6cefd8a
commit 7a4e5e7433
3 changed files with 287 additions and 19 deletions

View File

@ -256,10 +256,9 @@ QDateTime &QFileInfoPrivate::getFileTime(QAbstractFileEngine::FileTime request)
\snippet code/src_corelib_io_qfileinfo.cpp 0
On Windows, symlinks (shortcuts) are \c .lnk files. The reported
size() is that of the symlink (not the link's target), and
opening a symlink using QFile opens the \c .lnk file. For
example:
On Windows, shortcuts are \c .lnk files. The reported size() is that of
the shortcut (not the link's target), and opening a shortcut using QFile
opens the \c .lnk file. For example:
\snippet code/src_corelib_io_qfileinfo.cpp 1
@ -311,6 +310,19 @@ QDateTime &QFileInfoPrivate::getFileTime(QAbstractFileEngine::FileTime request)
\sa QDir, QFile
*/
/*!
\enum QFileInfo::FileType
This enum is returned by type() to describe the type of the file system
entity described by the QFileInfo object.
\value Unknown The object refers to an unknown item.
\value Regular The object refers to a regular file.
\value Directory The object refers to a directory.
\value SymbolicLink The object refers to a symbolic link.
\value Shortcut The object refers to a shortcut.
*/
/*!
\fn QFileInfo &QFileInfo::operator=(QFileInfo &&other)
@ -996,11 +1008,7 @@ bool QFileInfo::isNativePath() const
*/
bool QFileInfo::isFile() const
{
Q_D(const QFileInfo);
return d->checkAttribute<bool>(
QFileSystemMetaData::FileType,
[d]() { return d->metaData.isFile(); },
[d]() { return d->getFileFlags(QAbstractFileEngine::FileType); });
return (type() & FileTypeMask) == Regular;
}
/*!
@ -1011,11 +1019,7 @@ bool QFileInfo::isFile() const
*/
bool QFileInfo::isDir() const
{
Q_D(const QFileInfo);
return d->checkAttribute<bool>(
QFileSystemMetaData::DirectoryType,
[d]() { return d->metaData.isDirectory(); },
[d]() { return d->getFileFlags(QAbstractFileEngine::DirectoryType); });
return (type() & FileTypeMask) == Directory;
}
@ -1036,7 +1040,7 @@ bool QFileInfo::isBundle() const
}
/*!
Returns \c true if this object points to a symbolic link;
Returns \c true if this object points to a symbolic link or shortcut;
otherwise returns \c false.
Symbolic links exist on Unix (including \macos and iOS) and Windows
@ -1065,6 +1069,48 @@ bool QFileInfo::isSymLink() const
[d]() { return d->getFileFlags(QAbstractFileEngine::LinkType); });
}
/*!
\fn bool QFileInfo::isSymbolicLink() const
Returns \c true if this object points to a symbolic link;
otherwise returns \c false.
Symbolic links exist on Unix (including \macos and iOS) and Windows
(NTFS-symlink) and are typically created by the \c{ln -s} or \c{mklink}
commands, respectively.
Unix handles symlinks transparently. Opening a symbolic link effectively
opens the \l{symLinkTarget()}{link's target}.
In contrast to isSymLink(), false will be returned for shortcuts
(\c *.lnk files) on Windows. Use QFileInfo::isShortcut() instead.
\note If the symlink points to a non existing file, exists() returns
false.
\sa isFile(), isDir(), isShortcut(), symLinkTarget()
*/
/*!
\fn bool QFileInfo::isShortcut() const
Returns \c true if this object points to a shortcut;
otherwise returns \c false.
Shortcuts only exist on Windows and are typically \c .lnk files.
For instance, true will be returned for shortcuts (\c *.lnk files) on
Windows, but false will be returned on Unix (including \macos and iOS).
The shortcut (.lnk) files are treated as regular files. Opening those will
open the \c .lnk file itself. In order to open the file a shortcut
references to, it must uses symLinkTarget() on a shortcut.
\note Even if a shortcut (broken shortcut) points to a non existing file,
isShortcut() returns true.
\sa isFile(), isDir(), isSymbolicLink(), symLinkTarget()
*/
/*!
Returns \c true if the object points to a directory or to a symbolic
link to a directory, and that directory is the root directory; otherwise
@ -1268,6 +1314,53 @@ qint64 QFileInfo::size() const
});
}
/*!
Returns the QFileInfo::FileTypes.
QFileInfo::FileTypes combines with an indirection flag (link type) and a
base type it refers to.
For example, \c SymbolicLink combines with \c Regular meaning a symlink to
a regular file.
In addition, FileTypeMask and LinkTypeMask are used to extract the base
type and link type respectively.
\sa isFile(), isDir(), isShortcut(), isSymbolicLink()
*/
QFileInfo::FileTypes QFileInfo::type() const
{
Q_D(const QFileInfo);
QFileInfo::FileTypes type = QFileInfo::Unknown;
if (d->checkAttribute<bool>(
QFileSystemMetaData::LegacyLinkType,
[d]() { return d->metaData.isLnkFile(); },
[d]() { return d->getFileFlags(QAbstractFileEngine::LinkType); })) {
type = QFileInfo::Shortcut;
} else if (d->checkAttribute<bool>(
QFileSystemMetaData::LegacyLinkType,
[d]() { return d->metaData.isLink(); },
[d]() { return d->getFileFlags(QAbstractFileEngine::LinkType); })) {
type = QFileInfo::SymbolicLink;
}
if (d->checkAttribute<bool>(
QFileSystemMetaData::DirectoryType,
[d]() { return d->metaData.isDirectory(); },
[d]() { return d->getFileFlags(QAbstractFileEngine::DirectoryType); })) {
return type | QFileInfo::Directory;
}
if (d->checkAttribute<bool>(
QFileSystemMetaData::FileType,
[d]() { return d->metaData.isFile(); },
[d]() { return d->getFileFlags(QAbstractFileEngine::FileType); })) {
return type | QFileInfo::Regular;
}
return type;
}
#if QT_DEPRECATED_SINCE(5, 10)
/*!
\deprecated

View File

@ -66,6 +66,20 @@ public:
QFileInfo(const QFileInfo &fileinfo);
~QFileInfo();
enum FileType {
Unknown,
// base type
Regular,
Directory,
// indirection flag
SymbolicLink = 0x10,
Shortcut = 0x20,
// mask
FileTypeMask = 0x0f,
LinkTypeMask = 0xf0
};
Q_DECLARE_FLAGS(FileTypes, FileType)
QFileInfo &operator=(const QFileInfo &fileinfo);
QFileInfo &operator=(QFileInfo &&other) noexcept { swap(other); return *this; }
@ -111,6 +125,8 @@ public:
bool isFile() const;
bool isDir() const;
bool isSymLink() const;
inline bool isSymbolicLink() const { return type() & SymbolicLink; }
inline bool isShortcut() const { return type() & Shortcut; }
bool isRoot() const;
bool isBundle() const;
@ -129,6 +145,7 @@ public:
QFile::Permissions permissions() const;
qint64 size() const;
FileTypes type() const;
// ### Qt6: inline these functions
#if QT_DEPRECATED_SINCE(5, 10)

View File

@ -236,6 +236,8 @@ private slots:
void isSymLink_data();
void isSymLink();
void link_data();
void link();
void isHidden_data();
void isHidden();
@ -277,6 +279,9 @@ private slots:
void invalidState();
void nonExistingFile();
void type_data();
void type();
private:
const QString m_currentDir;
QString m_sourceFile;
@ -1337,6 +1342,88 @@ void tst_QFileInfo::isSymLink()
#endif
}
Q_DECLARE_METATYPE(QFileInfo::FileType)
void tst_QFileInfo::link_data()
{
QFile::remove("link");
QFile::remove("link.lnk");
QFile::remove("brokenlink");
QFile::remove("brokenlink.lnk");
QFile::remove("dummyfile");
QFile::remove("relative/link");
QTest::addColumn<QString>("path");
QTest::addColumn<QFileInfo::FileType>("linkType");
QTest::addColumn<QString>("linkTarget");
QFile file1(m_sourceFile);
QFile file2("dummyfile");
file2.open(QIODevice::WriteOnly);
QTest::newRow("existent file") << m_sourceFile << QFileInfo::Unknown << "";
#if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
// windows shortcuts
QVERIFY(file1.link("link.lnk"));
QTest::newRow("link.lnk")
<< "link.lnk" << QFileInfo::Shortcut << QFileInfo(m_sourceFile).absoluteFilePath();
QVERIFY(file2.link("brokenlink.lnk"));
QTest::newRow("broken link.lnk")
<< "brokenlink.lnk" << QFileInfo::Shortcut << QFileInfo("dummyfile").absoluteFilePath();
#endif
#ifndef Q_NO_SYMLINKS
#if defined(Q_OS_WIN)
#if !defined(Q_OS_WINRT)
QString errorMessage;
DWORD creationResult = createSymbolicLink("link", m_sourceFile, &errorMessage);
if (creationResult == ERROR_PRIVILEGE_NOT_HELD) {
QWARN(msgInsufficientPrivileges(errorMessage));
} else {
QVERIFY2(creationResult == ERROR_SUCCESS, qPrintable(errorMessage));
QTest::newRow("link")
<< "link" << QFileInfo::SymbolicLink << QFileInfo(m_sourceFile).absoluteFilePath();
}
creationResult = createSymbolicLink("brokenlink", "dummyfile", &errorMessage);
if (creationResult == ERROR_PRIVILEGE_NOT_HELD) {
QWARN(msgInsufficientPrivileges(errorMessage));
} else {
QVERIFY2(creationResult == ERROR_SUCCESS, qPrintable(errorMessage));
QTest::newRow("broken link")
<< "brokenlink" << QFileInfo::SymbolicLink << QFileInfo("dummyfile").absoluteFilePath();
}
#endif // !Q_OS_WINRT
#else // Unix:
QVERIFY(file1.link("link"));
QTest::newRow("link")
<< "link" << QFileInfo::SymbolicLink << QFileInfo(m_sourceFile).absoluteFilePath();
QVERIFY(file2.link("brokenlink"));
QTest::newRow("broken link")
<< "brokenlink" << QFileInfo::SymbolicLink << QFileInfo("dummyfile").absoluteFilePath();
QDir::current().mkdir("relative");
QFile::link("../dummyfile", "relative/link");
QTest::newRow("relative link")
<< "relative/link" << QFileInfo::SymbolicLink << QFileInfo("dummyfile").absoluteFilePath();
#endif
#endif // !Q_NO_SYMLINKS
file2.remove();
}
void tst_QFileInfo::link()
{
QFETCH(QString, path);
QFETCH(QFileInfo::FileType, linkType);
QFETCH(QString, linkTarget);
QFileInfo fi(path);
QCOMPARE(fi.type() & QFileInfo::LinkTypeMask, linkType);
QCOMPARE(fi.symLinkTarget(), linkTarget);
}
void tst_QFileInfo::isHidden_data()
{
QTest::addColumn<QString>("path");
@ -1638,7 +1725,7 @@ void tst_QFileInfo::ntfsJunctionPointsAndSymlinks()
QVERIFY2(creationResult == ERROR_SUCCESS, qPrintable(errorMessage));
QFileInfo fi(path);
const bool actualIsSymLink = fi.isSymLink();
const bool actualIsSymLink = fi.isSymbolicLink();
const QString actualSymLinkTarget = isSymLink ? fi.symLinkTarget() : QString();
const QString actualCanonicalFilePath = isSymLink ? fi.canonicalFilePath() : QString();
// Ensure that junctions, mountpoints are removed. If this fails, do not remove
@ -1667,14 +1754,16 @@ void tst_QFileInfo::brokenShortcut()
file.close();
QFileInfo info(linkName);
QVERIFY(info.isSymLink());
QVERIFY(!info.isSymbolicLink());
QVERIFY(info.isShortcut());
QVERIFY(!info.exists());
QFile::remove(linkName);
QDir current; // QTBUG-21863
QVERIFY(current.mkdir(linkName));
QFileInfo dirInfo(linkName);
QVERIFY(!dirInfo.isSymLink());
QVERIFY(!dirInfo.isSymbolicLink());
QVERIFY(!dirInfo.isShortcut());
QVERIFY(dirInfo.isDir());
current.rmdir(linkName);
}
@ -2032,7 +2121,8 @@ static void stateCheck(const QFileInfo &info, const QString &dirname, const QStr
QVERIFY(!info.isHidden());
QVERIFY(!info.isFile());
QVERIFY(!info.isDir());
QVERIFY(!info.isSymLink());
QVERIFY(!info.isSymbolicLink());
QVERIFY(!info.isShortcut());
QVERIFY(!info.isBundle());
QVERIFY(!info.isRoot());
QCOMPARE(info.isNativePath(), !filename.isEmpty());
@ -2089,5 +2179,73 @@ void tst_QFileInfo::nonExistingFile()
stateCheck(info, dirname, filename);
}
Q_DECLARE_METATYPE(QFileInfo::FileTypes)
void tst_QFileInfo::type_data()
{
QFile::remove("link.lnk");
QFile::remove("symlink.lnk");
QFile::remove("link");
QFile::remove("symlink");
QFile::remove("directory.lnk");
QFile::remove("directory");
QTest::addColumn<QString>("path");
QTest::addColumn<QFileInfo::FileTypes>("type");
QFile regularFile(m_sourceFile);
QTest::newRow("regular")
<< regularFile.fileName() << QFileInfo::FileTypes(QFileInfo::Regular);
QTest::newRow("directory")
<< QDir::currentPath() << QFileInfo::FileTypes(QFileInfo::Directory);
#if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
// windows shortcuts
QVERIFY(regularFile.link("link.lnk"));
QTest::newRow("shortcut")
<< "link.lnk" << QFileInfo::FileTypes(QFileInfo::Shortcut | QFileInfo::Regular);
QVERIFY(regularFile.link("link"));
QTest::newRow("invalid-shortcut")
<< "link" << QFileInfo::FileTypes(QFileInfo::Regular);
QVERIFY(QFile::link(QDir::currentPath(), "directory.lnk"));
QTest::newRow("directory-shortcut")
<< "directory.lnk" << QFileInfo::FileTypes(QFileInfo::Shortcut | QFileInfo::Directory);
#endif
#ifndef Q_NO_SYMLINKS
#if defined(Q_OS_WIN)
#if !defined(Q_OS_WINRT)
QString errorMessage;
const DWORD creationResult = createSymbolicLink("symlink", m_sourceFile, &errorMessage);
if (creationResult == ERROR_PRIVILEGE_NOT_HELD) {
QWARN(msgInsufficientPrivileges(errorMessage));
} else {
QVERIFY2(creationResult == ERROR_SUCCESS, qPrintable(errorMessage));
QTest::newRow("NTFS-symlink")
<< "symlink" << QFileInfo::FileTypes(QFileInfo::SymbolicLink | QFileInfo::Regular);
}
#endif // !Q_OS_WINRT
#else // Unix:
QVERIFY(regularFile.link("symlink.lnk"));
QTest::newRow("symlink.lnk")
<< "symlink.lnk" << QFileInfo::FileTypes(QFileInfo::SymbolicLink | QFileInfo::Regular);
QVERIFY(regularFile.link("symlink"));
QTest::newRow("symlink")
<< "symlink" << QFileInfo::FileTypes(QFileInfo::SymbolicLink | QFileInfo::Regular);
QVERIFY(QFile::link(QDir::currentPath(), "directory"));
QTest::newRow("directory-symlink")
<< "directory" << QFileInfo::FileTypes(QFileInfo::SymbolicLink | QFileInfo::Directory);
#endif
#endif // !Q_NO_SYMLINKS
}
void tst_QFileInfo::type()
{
QFETCH(QString, path);
QFETCH(QFileInfo::FileTypes, type);
QFileInfo info(path);
QCOMPARE(info.type(), type);
}
QTEST_MAIN(tst_QFileInfo)
#include "tst_qfileinfo.moc"