summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThiago Macieira <thiago.macieira@intel.com>2025-08-06 08:59:10 -0700
committerThiago Macieira <thiago.macieira@intel.com>2025-08-26 13:17:17 -0700
commitc86a33db043cb20ceea0ee752dec9548c3a85259 (patch)
tree890d623a0cac782b76f230555ddc72ad413527c7
parent2e3afe3f02b2cdbee1615ad7ef47ef47a0506804 (diff)
moveToTrash/XDG: allow $XDG_DATA_HOME/Trash to be a symlink
This is in the user's $HOME dir and thus entirely controlled by the user, so we don't need to apply the strict protections that the XDG spec requires of other dirs. The trash spec[1] being one long section without structures made the reading ambiguous whether the checks should also be applied to $HOME. This commit also allows the "info" and "files" subdirs to be symlinks, but I'm not testing that. [ChangeLog][QtCore][QFile] Fixed a bug that caused moveToTrash() to disallow trashing to trash bins using the XDG Trash Specification when the trash path was a symlink. [1] https://specifications.freedesktop.org/trash-spec/1.0/ Pick-to: 6.8 6.9 6.10 Fixes: QTBUG-138984 Change-Id: I4f05391082bc607389e0fffd06a9b72fdd098f22 Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io> Reviewed-by: David Faure <david.faure@kdab.com>
-rw-r--r--src/corelib/io/qfilesystemengine_unix.cpp24
-rw-r--r--tests/auto/corelib/io/qfile/tst_qfile.cpp48
2 files changed, 61 insertions, 11 deletions
diff --git a/src/corelib/io/qfilesystemengine_unix.cpp b/src/corelib/io/qfilesystemengine_unix.cpp
index a5e2f47b485..a54c96a711d 100644
--- a/src/corelib/io/qfilesystemengine_unix.cpp
+++ b/src/corelib/io/qfilesystemengine_unix.cpp
@@ -1439,17 +1439,17 @@ struct FreeDesktopTrashOperation
}
// opens a directory and returns the file descriptor
- static int openDirFd(int dfd, const char *path, int mode = 0)
+ static int openDirFd(int dfd, const char *path, int mode)
{
- mode |= QT_OPEN_RDONLY | O_NOFOLLOW | O_DIRECTORY;
+ mode |= QT_OPEN_RDONLY | O_DIRECTORY;
return qt_safe_openat(dfd, path, mode);
}
// opens an XDG Trash directory that is a subdirectory of dfd, creating if necessary
- static int openOrCreateDir(int dfd, const char *path)
+ static int openOrCreateDir(int dfd, const char *path, int openmode = 0)
{
// try to open it as a dir, first
- int fd = openDirFd(dfd, path);
+ int fd = openDirFd(dfd, path, openmode);
if (fd >= 0 || errno != ENOENT)
return fd;
@@ -1458,19 +1458,19 @@ struct FreeDesktopTrashOperation
return -1;
// try to open it again
- return openDirFd(dfd, path);
+ return openDirFd(dfd, path, openmode);
}
// opens or makes the XDG Trash hierarchy on parentfd (may be -1) called targetDir
bool getTrashDir(int parentfd, QString targetDir, const QFileSystemEntry &source,
- QSystemError &error)
+ int openmode, QSystemError &error)
{
if (parentfd == AT_FDCWD)
trashPath = targetDir;
QByteArray nativePath = QFile::encodeName(targetDir);
// open the directory
- int trashfd = openOrCreateDir(parentfd, nativePath);
+ int trashfd = openOrCreateDir(parentfd, nativePath, openmode);
if (trashfd < 0 && errno != ENOENT) {
error = QSystemError(errno, QSystemError::StandardLibraryError);
return false;
@@ -1535,7 +1535,7 @@ struct FreeDesktopTrashOperation
QFileSystemEntry dotTrashDir(sourceStorage.rootPath() + dotTrash);
// we MUST check that the sticky bit is set, and that it is not a symlink
- int genericTrashFd = openDirFd(AT_FDCWD, dotTrashDir.nativeFilePath());
+ int genericTrashFd = openDirFd(AT_FDCWD, dotTrashDir.nativeFilePath(), O_NOFOLLOW);
QT_STATBUF st = {};
if (genericTrashFd < 0 && errno != ENOENT && errno != EACCES) {
// O_DIRECTORY + O_NOFOLLOW produces ENOTDIR on Linux
@@ -1563,7 +1563,7 @@ struct FreeDesktopTrashOperation
the implementation MUST immediately create it, without any warnings or
delays for the user."
*/
- if (getTrashDir(genericTrashFd, userID, source, error)) {
+ if (getTrashDir(genericTrashFd, userID, source, O_NOFOLLOW, error)) {
// recreate the resulting path
trashPath = dotTrashDir.filePath() + u'/' + userID;
}
@@ -1579,7 +1579,8 @@ struct FreeDesktopTrashOperation
immediately create it, without any warnings or delays for the user."
*/
if (!isTrashDirOpen())
- getTrashDir(AT_FDCWD, sourceStorage.rootPath() + dotTrash + u'-' + userID, source, error);
+ getTrashDir(AT_FDCWD, sourceStorage.rootPath() + dotTrash + u'-' + userID, source,
+ O_NOFOLLOW, error);
if (isTrashDirOpen()) {
volumePrefixLength = sourceStorage.rootPath().size();
@@ -1594,7 +1595,8 @@ struct FreeDesktopTrashOperation
bool openHomeTrashLocation(const QFileSystemEntry &source, QSystemError &error)
{
QString topDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
- return getTrashDir(AT_FDCWD, topDir + "/Trash"_L1, source, error);
+ int openmode = 0; // do allow following symlinks
+ return getTrashDir(AT_FDCWD, topDir + "/Trash"_L1, source, openmode, error);
}
bool findTrashFor(const QFileSystemEntry &source, QSystemError &error)
diff --git a/tests/auto/corelib/io/qfile/tst_qfile.cpp b/tests/auto/corelib/io/qfile/tst_qfile.cpp
index 843b1294501..882fce81db5 100644
--- a/tests/auto/corelib/io/qfile/tst_qfile.cpp
+++ b/tests/auto/corelib/io/qfile/tst_qfile.cpp
@@ -301,6 +301,7 @@ private slots:
void moveToTrashSymlinkToFile();
void moveToTrashSymlinkToDirectory_data();
void moveToTrashSymlinkToDirectory();
+ void moveToTrashXdgHomeTrashIsSymlink();
void moveToTrashXdgSafety();
void stdfilesystem();
@@ -4409,6 +4410,53 @@ void tst_QFile::moveToTrashSymlinkToDirectory()
cleanLink.dismiss();
}
+void tst_QFile::moveToTrashXdgHomeTrashIsSymlink()
+{
+ if (!QFile::supportsMoveToTrash())
+ QSKIP("This platform doesn't implement a trash bin");
+
+#if defined(Q_OS_WIN) || defined(Q_OS_DARWIN) || defined(Q_OS_ANDROID) || defined(Q_OS_WEBOS)
+ QSKIP("This test is specific to XDG Unix systems");
+#else
+ if (!QStandardPaths::isTestModeEnabled())
+ QFAIL("Constructor should have enabled test mode");
+
+ QString xdgDataHome = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
+ Q_ASSERT(xdgDataHome.contains("qttest/"));
+ QString xdgHomeTrash = xdgDataHome + "/Trash"_L1;
+ QString tempPattern = xdgDataHome + "/tst_qfile_moveToTrashXdgHomeTrashIsSymlink.XXXXXX";
+ auto removeTrashAsSymlink = [&xdgHomeTrash] {
+ QFile::remove(xdgHomeTrash);
+ };
+
+ // create a file for us to trash
+ QTemporaryFile fileToTrash(tempPattern);
+ QVERIFY2(fileToTrash.open(), qPrintable(fileToTrash.errorString()));
+
+ // obliterate the test-mode home trash and create a symlink in its place
+ if (QFileInfo fi(xdgHomeTrash); fi.isSymLink() || !fi.isDir())
+ removeTrashAsSymlink();
+ else
+ QDir(xdgHomeTrash).removeRecursively();
+
+ QTemporaryDir otherTrash(tempPattern);
+ QVERIFY2(otherTrash.isValid(), qPrintable(otherTrash.errorString()));
+ if (QFile src(otherTrash.path()); true)
+ QVERIFY2(src.link(xdgHomeTrash), qPrintable(src.errorString()));
+ auto deleteSymlink = qScopeGuard(removeTrashAsSymlink);
+
+ // we should be able to trash an open file in XDG platforms (test above)
+
+ QFile f(fileToTrash.fileName());
+ QVERIFY2(f.moveToTrash(), qPrintable(f.errorString()));
+ QVERIFY(f.exists());
+
+ QVERIFY(!QFileInfo(fileToTrash.fileName()).exists());
+ QVERIFY(QFile(otherTrash.filePath("files")).exists());
+ QVERIFY(QFile(otherTrash.filePath("info")).exists());
+#endif
+}
+
void tst_QFile::moveToTrashXdgSafety()
{
if (!QFile::supportsMoveToTrash())