diff options
Diffstat (limited to 'src/corelib/io/qfilesystemengine_unix.cpp')
-rw-r--r-- | src/corelib/io/qfilesystemengine_unix.cpp | 626 |
1 files changed, 369 insertions, 257 deletions
diff --git a/src/corelib/io/qfilesystemengine_unix.cpp b/src/corelib/io/qfilesystemengine_unix.cpp index 9036608e7d..bda2962f8d 100644 --- a/src/corelib/io/qfilesystemengine_unix.cpp +++ b/src/corelib/io/qfilesystemengine_unix.cpp @@ -7,21 +7,26 @@ #include "qfilesystemengine_p.h" #include "qfile.h" #include "qstorageinfo.h" +#include "qurl.h" #include <QtCore/qoperatingsystemversion.h> #include <QtCore/private/qcore_unix_p.h> #include <QtCore/private/qfiledevice_p.h> +#include <QtCore/private/qfunctions_p.h> #include <QtCore/qvarlengtharray.h> #ifndef QT_BOOTSTRAPPED # include <QtCore/qstandardpaths.h> +# include <QtCore/private/qtemporaryfile_p.h> #endif // QT_BOOTSTRAPPED +#include <grp.h> #include <pwd.h> #include <stdlib.h> // for realpath() #include <unistd.h> #include <stdio.h> #include <errno.h> +#include <chrono> #include <memory> // for std::unique_ptr #if __has_include(<paths.h>) @@ -31,9 +36,14 @@ # define _PATH_TMP "/tmp" #endif -#if defined(Q_OS_MAC) +#if defined(Q_OS_DARWIN) # include <QtCore/private/qcore_mac_p.h> # include <CoreFoundation/CFBundle.h> +# include <UniformTypeIdentifiers/UTType.h> +# include <UniformTypeIdentifiers/UTCoreTypes.h> +# include <Foundation/Foundation.h> +# include <sys/clonefile.h> +# include <copyfile.h> #endif #ifdef Q_OS_MACOS @@ -44,15 +54,6 @@ #include <MobileCoreServices/MobileCoreServices.h> #endif -#if defined(Q_OS_DARWIN) -# include <sys/clonefile.h> -# include <copyfile.h> -// We cannot include <Foundation/Foundation.h> (it's an Objective-C header), but -// we need these declarations: -Q_FORWARD_DECLARE_OBJC_CLASS(NSString); -extern "C" NSString *NSTemporaryDirectory(); -#endif - #if defined(Q_OS_LINUX) # include <sys/ioctl.h> # include <sys/sendfile.h> @@ -122,10 +123,9 @@ static bool isPackage(const QFileSystemMetaData &data, const QFileSystemEntry &e QString suffix = info.suffix(); if (suffix.length() > 0) { - // First step: is the extension known ? - QCFType<CFStringRef> extensionRef = suffix.toCFString(); - QCFType<CFStringRef> uniformTypeIdentifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, extensionRef, NULL); - if (UTTypeConformsTo(uniformTypeIdentifier, kUTTypeBundle)) + // First step: is it a bundle? + const auto *utType = [UTType typeWithFilenameExtension:suffix.toNSString()]; + if ([utType conformsToType:UTTypeBundle]) return true; // Second step: check if an application knows the package type @@ -159,51 +159,39 @@ static bool isPackage(const QFileSystemMetaData &data, const QFileSystemEntry &e namespace { namespace GetFileTimes { -#if !QT_CONFIG(futimens) && (QT_CONFIG(futimes)) -template <typename T> -static inline typename std::enable_if_t<(&T::st_atim, &T::st_mtim, true)> get(const T *p, struct timeval *access, struct timeval *modification) +qint64 time_t_toMsecs(time_t t) { - access->tv_sec = p->st_atim.tv_sec; - access->tv_usec = p->st_atim.tv_nsec / 1000; - - modification->tv_sec = p->st_mtim.tv_sec; - modification->tv_usec = p->st_mtim.tv_nsec / 1000; + using namespace std::chrono; + return milliseconds{seconds{t}}.count(); } -template <typename T> -static inline typename std::enable_if_t<(&T::st_atimespec, &T::st_mtimespec, true)> get(const T *p, struct timeval *access, struct timeval *modification) +// fallback set +[[maybe_unused]] qint64 atime(const QT_STATBUF &statBuffer, ulong) { - access->tv_sec = p->st_atimespec.tv_sec; - access->tv_usec = p->st_atimespec.tv_nsec / 1000; - - modification->tv_sec = p->st_mtimespec.tv_sec; - modification->tv_usec = p->st_mtimespec.tv_nsec / 1000; + return time_t_toMsecs(statBuffer.st_atime); } - -# ifndef st_atimensec -// if "st_atimensec" is defined, this would expand to invalid C++ -template <typename T> -static inline typename std::enable_if_t<(&T::st_atimensec, &T::st_mtimensec, true)> get(const T *p, struct timeval *access, struct timeval *modification) +[[maybe_unused]] qint64 birthtime(const QT_STATBUF &, ulong) { - access->tv_sec = p->st_atime; - access->tv_usec = p->st_atimensec / 1000; - - modification->tv_sec = p->st_mtime; - modification->tv_usec = p->st_mtimensec / 1000; + return Q_INT64_C(0); } -# endif -#endif - -qint64 timespecToMSecs(const timespec &spec) +[[maybe_unused]] qint64 ctime(const QT_STATBUF &statBuffer, ulong) { - return (qint64(spec.tv_sec) * 1000) + (spec.tv_nsec / 1000000); + return time_t_toMsecs(statBuffer.st_ctime); +} +[[maybe_unused]] qint64 mtime(const QT_STATBUF &statBuffer, ulong) +{ + return time_t_toMsecs(statBuffer.st_mtime); } -// fallback set -[[maybe_unused]] qint64 atime(const QT_STATBUF &statBuffer, ulong) { return qint64(statBuffer.st_atime) * 1000; } -[[maybe_unused]] qint64 birthtime(const QT_STATBUF &, ulong) { return Q_INT64_C(0); } -[[maybe_unused]] qint64 ctime(const QT_STATBUF &statBuffer, ulong) { return qint64(statBuffer.st_ctime) * 1000; } -[[maybe_unused]] qint64 mtime(const QT_STATBUF &statBuffer, ulong) { return qint64(statBuffer.st_mtime) * 1000; } +// T is either a stat.timespec or statx.statx_timestamp, +// both have tv_sec and tv_nsec members +template<typename T> +qint64 timespecToMSecs(const T &spec) +{ + using namespace std::chrono; + const nanoseconds nsecs = seconds{spec.tv_sec} + nanoseconds{spec.tv_nsec}; + return duration_cast<milliseconds>(nsecs).count(); +} // Xtim, POSIX.1-2008 template <typename T> @@ -338,17 +326,12 @@ inline void QFileSystemMetaData::fillFromStatxBuf(const struct statx &statxBuffe size_ = qint64(statxBuffer.stx_size); // Times - auto toMSecs = [](struct statx_timestamp ts) - { - return qint64(ts.tv_sec) * 1000 + (ts.tv_nsec / 1000000); - }; - accessTime_ = toMSecs(statxBuffer.stx_atime); - metadataChangeTime_ = toMSecs(statxBuffer.stx_ctime); - modificationTime_ = toMSecs(statxBuffer.stx_mtime); - if (statxBuffer.stx_mask & STATX_BTIME) - birthTime_ = toMSecs(statxBuffer.stx_btime); - else - birthTime_ = 0; + using namespace GetFileTimes; + accessTime_ = timespecToMSecs(statxBuffer.stx_atime); + metadataChangeTime_ = timespecToMSecs(statxBuffer.stx_ctime); + modificationTime_ = timespecToMSecs(statxBuffer.stx_mtime); + const bool birthMask = statxBuffer.stx_mask & STATX_BTIME; + birthTime_ = birthMask ? timespecToMSecs(statxBuffer.stx_btime) : 0; userId_ = statxBuffer.stx_uid; groupId_ = statxBuffer.stx_gid; @@ -591,7 +574,7 @@ QFileSystemEntry QFileSystemEngine::getLinkTarget(const QFileSystemEntry &link, Q_CHECK_FILE_NAME(link, link); QByteArray s = qt_readlink(link.nativeFilePath().constData()); - if (s.length() > 0) { + if (s.size() > 0) { QString ret; if (!data.hasFlags(QFileSystemMetaData::DirectoryType)) fillMetaData(link, data, QFileSystemMetaData::DirectoryType); @@ -645,11 +628,21 @@ QFileSystemEntry QFileSystemEngine::getLinkTarget(const QFileSystemEntry &link, } //static +QFileSystemEntry QFileSystemEngine::getRawLinkPath(const QFileSystemEntry &link, + QFileSystemMetaData &data) +{ + Q_UNUSED(data) + const QByteArray path = qt_readlink(link.nativeFilePath().constData()); + const QString ret = QFile::decodeName(path); + return QFileSystemEntry(ret); +} + +//static QFileSystemEntry QFileSystemEngine::canonicalName(const QFileSystemEntry &entry, QFileSystemMetaData &data) { Q_CHECK_FILE_NAME(entry, entry); -#if !defined(Q_OS_MAC) && !defined(Q_OS_QNX) && !defined(Q_OS_ANDROID) && !defined(Q_OS_HAIKU) && _POSIX_VERSION < 200809L +#if !defined(Q_OS_DARWIN) && !defined(Q_OS_QNX) && !defined(Q_OS_ANDROID) && !defined(Q_OS_HAIKU) && _POSIX_VERSION < 200809L // realpath(X,0) is not supported Q_UNUSED(data); return QFileSystemEntry(slowCanonicalized(absoluteName(entry).filePath())); @@ -713,13 +706,13 @@ QFileSystemEntry QFileSystemEngine::absoluteName(const QFileSystemEntry &entry) QFileSystemEntry cur(currentPath()); result = cur.nativeFilePath(); } - if (!orig.isEmpty() && !(orig.length() == 1 && orig[0] == '.')) { + if (!orig.isEmpty() && !(orig.size() == 1 && orig[0] == '.')) { if (!result.isEmpty() && !result.endsWith('/')) result.append('/'); result.append(orig); } - if (result.length() == 1 && result[0] == '/') + if (result.size() == 1 && result[0] == '/') return QFileSystemEntry(result, QFileSystemEntry::FromNativePath()); const bool isDir = result.endsWith('/'); @@ -769,7 +762,7 @@ QByteArray QFileSystemEngine::id(int fd) QString QFileSystemEngine::resolveUserName(uint userId) { #if QT_CONFIG(thread) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_OPENBSD) - int size_max = sysconf(_SC_GETPW_R_SIZE_MAX); + long size_max = sysconf(_SC_GETPW_R_SIZE_MAX); if (size_max == -1) size_max = 1024; QVarLengthArray<char, 1024> buf(size_max); @@ -795,7 +788,7 @@ QString QFileSystemEngine::resolveUserName(uint userId) QString QFileSystemEngine::resolveGroupName(uint groupId) { #if QT_CONFIG(thread) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_OPENBSD) - int size_max = sysconf(_SC_GETPW_R_SIZE_MAX); + long size_max = sysconf(_SC_GETPW_R_SIZE_MAX); if (size_max == -1) size_max = 1024; QVarLengthArray<char, 1024> buf(size_max); @@ -811,7 +804,7 @@ QString QFileSystemEngine::resolveGroupName(uint groupId) struct group entry; // Some large systems have more members than the POSIX max size // Loop over by doubling the buffer size (upper limit 250k) - for (unsigned size = size_max; size < 256000; size += size) + for (long size = size_max; size < 256000; size += size) { buf.resize(size); // ERANGE indicates that the buffer was too small @@ -824,7 +817,7 @@ QString QFileSystemEngine::resolveGroupName(uint groupId) #endif if (gr) return QFile::decodeName(QByteArray(gr->gr_name)); -#else // Integrity || WASM +#else // Integrity || WASM || VxWorks Q_UNUSED(groupId); #endif return QString(); @@ -1109,7 +1102,7 @@ static bool createDirectoryWithParents(const QByteArray &nativeName, mode_t mode return false; // mkdir failed because the parent dir doesn't exist, so try to create it - int slash = nativeName.lastIndexOf('/'); + qsizetype slash = nativeName.lastIndexOf('/'); if (slash < 1) return false; @@ -1127,7 +1120,7 @@ static bool createDirectoryWithParents(const QByteArray &nativeName, mode_t mode bool QFileSystemEngine::createDirectory(const QFileSystemEntry &entry, bool createParents, std::optional<QFile::Permissions> permissions) { - QString dirName = entry.filePath(); + QByteArray dirName = entry.nativeFilePath(); Q_CHECK_FILE_NAME(dirName, false); // Darwin doesn't support trailing /'s, so remove for everyone @@ -1135,14 +1128,13 @@ bool QFileSystemEngine::createDirectory(const QFileSystemEntry &entry, bool crea dirName.chop(1); // try to mkdir this directory - QByteArray nativeName = QFile::encodeName(dirName); mode_t mode = permissions ? QtPrivate::toMode_t(*permissions) : 0777; - if (QT_MKDIR(nativeName, mode) == 0) + if (QT_MKDIR(dirName, mode) == 0) return true; if (!createParents) return false; - return createDirectoryWithParents(nativeName, mode, false); + return createDirectoryWithParents(dirName, mode, false); } //static @@ -1152,7 +1144,7 @@ bool QFileSystemEngine::removeDirectory(const QFileSystemEntry &entry, bool remo if (removeEmptyParents) { QString dirName = QDir::cleanPath(entry.filePath()); - for (int oldslash = 0, slash=dirName.length(); slash > 0; oldslash = slash) { + for (qsizetype oldslash = 0, slash=dirName.size(); slash > 0; oldslash = slash) { const QByteArray chunk = QFile::encodeName(dirName.left(slash)); QT_STATBUF st; if (QT_STAT(chunk.constData(), &st) != -1) { @@ -1182,42 +1174,169 @@ bool QFileSystemEngine::createLink(const QFileSystemEntry &source, const QFileSy return false; } -#ifndef Q_OS_DARWIN +#ifdef Q_OS_DARWIN +// see qfilesystemengine_mac.mm +#elif defined(QT_BOOTSTRAPPED) || !defined(AT_FDCWD) +// bootstrapped tools don't need this, and we don't want QStorageInfo +//static +bool QFileSystemEngine::moveFileToTrash(const QFileSystemEntry &, QFileSystemEntry &, + QSystemError &error) +{ + error = QSystemError(ENOSYS, QSystemError::StandardLibraryError); + return false; +} +#else /* Implementing as per https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html */ -// bootstrapped tools don't need this, and we don't want QStorageInfo -#ifndef QT_BOOTSTRAPPED -static QString freeDesktopTrashLocation(const QString &sourcePath) +namespace { +struct FreeDesktopTrashOperation { - auto makeTrashDir = [](const QDir &topDir, const QString &trashDir) -> QString { - auto ownerPerms = QFileDevice::ReadOwner - | QFileDevice::WriteOwner - | QFileDevice::ExeOwner; - QString targetDir = topDir.filePath(trashDir); - // deliberately not using mkpath, since we want to fail if topDir doesn't exist - if (topDir.mkdir(trashDir)) - QFile::setPermissions(targetDir, ownerPerms); - if (QFileInfo(targetDir).isDir()) - return targetDir; - return QString(); - }; - auto isSticky = [](const QFileInfo &fileInfo) -> bool { - struct stat st; - if (stat(QFile::encodeName(fileInfo.absoluteFilePath()).constData(), &st) == 0) - return st.st_mode & S_ISVTX; + /* + "A trash directory contains two subdirectories, named info and files." + */ + QString trashPath; + int filesDirFd = -1; + int infoDirFd = -1; + qsizetype volumePrefixLength = 0; - return false; - }; + // relative file paths to the filesDirFd and infoDirFd from above + QByteArray tempTrashFileName; + QByteArray infoFilePath; + + int infoFileFd = -1; // if we've already opened it + ~FreeDesktopTrashOperation() + { + close(); + } + + constexpr bool isTrashDirOpen() const { return filesDirFd != -1 && infoDirFd != -1; } + + void close() + { + int savedErrno = errno; + if (infoFileFd != -1) { + Q_ASSERT(infoDirFd != -1); + Q_ASSERT(!infoFilePath.isEmpty()); + Q_ASSERT(!trashPath.isEmpty()); + + QT_CLOSE(infoFileFd); + unlinkat(infoDirFd, infoFilePath, 0); + infoFileFd = -1; + } + if (!tempTrashFileName.isEmpty()) { + Q_ASSERT(filesDirFd != -1); + unlinkat(filesDirFd, tempTrashFileName, 0); + } + if (filesDirFd >= 0) + QT_CLOSE(filesDirFd); + if (infoDirFd >= 0) + QT_CLOSE(infoDirFd); + filesDirFd = infoDirFd = -1; + errno = savedErrno; + } + + bool tryCreateInfoFile(const QString &filePath, QSystemError &error) + { + QByteArray p = QFile::encodeName(filePath) + ".trashinfo"; + infoFileFd = qt_safe_openat(infoDirFd, p, QT_OPEN_RDWR | QT_OPEN_CREAT | QT_OPEN_EXCL, 0666); + if (infoFileFd < 0) { + error = QSystemError(errno, QSystemError::StandardLibraryError); + return false; + } + infoFilePath = std::move(p); + return true; + } + + void commit() + { + QT_CLOSE(infoFileFd); + infoFileFd = -1; + tempTrashFileName = {}; + } - QString trash; - const QStorageInfo sourceStorage(sourcePath); - const QStorageInfo homeStorage(QDir::home()); - // We support trashing of files outside the users home partition - if (sourceStorage != homeStorage) { - const auto dotTrash = ".Trash"_L1; - QDir topDir(sourceStorage.rootPath()); + // opens a directory and returns the file descriptor + static int openDirFd(int dfd, const char *path, int mode = 0) + { + mode |= QT_OPEN_RDONLY | O_NOFOLLOW | 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) + { + // try to open it as a dir, first + int fd = openDirFd(dfd, path); + if (fd >= 0 || errno != ENOENT) + return fd; + + // try to mkdirat + if (mkdirat(dfd, path, 0700) < 0) + return -1; + + // try to open it again + return openDirFd(dfd, path); + } + + // 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) + { + if (parentfd == AT_FDCWD) + trashPath = targetDir; + QByteArray nativePath = QFile::encodeName(targetDir); + + // open the directory + int trashfd = openOrCreateDir(parentfd, nativePath); + if (trashfd < 0 && errno != ENOENT) { + error = QSystemError(errno, QSystemError::StandardLibraryError); + return false; + } + + // check if it is ours (even if we've just mkdirat'ed it) + if (QT_STATBUF st; QT_FSTAT(trashfd, &st) < 0) { + error = QSystemError(errno, QSystemError::StandardLibraryError); + return false; + } else if (st.st_uid != getuid()) { + error = QSystemError(EPERM, QSystemError::StandardLibraryError); + return false; + } + + filesDirFd = openOrCreateDir(trashfd, "files"); + if (filesDirFd >= 0) { + // try to link our file-to-be-trashed here + QTemporaryFileName tfn("XXXXXX"_L1); + for (int i = 0; i < 16; ++i) { + QByteArray attempt = tfn.generateNext(); + if (linkat(AT_FDCWD, source.nativeFilePath(), filesDirFd, attempt, 0) == 0) { + tempTrashFileName = std::move(attempt); + break; + } + if (errno != EEXIST) + break; + } + + // man 2 link on Linux has: + // EPERM The filesystem containing oldpath and newpath does not + // support the creation of hard links. + // EPERM oldpath is a directory. + // EPERM oldpath is marked immutable or append‐only. + // EMLINK The file referred to by oldpath already has the maximum + // number of links to it. + if (!tempTrashFileName.isEmpty() || errno == EPERM || errno == EMLINK) + infoDirFd = openOrCreateDir(trashfd, "info"); + } + error = QSystemError(errno, QSystemError::StandardLibraryError); + if (infoDirFd < 0) + close(); + QT_CLOSE(trashfd); + return infoDirFd >= 0; + } + + bool openMountPointTrashLocation(const QFileSystemEntry &source, + const QStorageInfo &sourceStorage, QSystemError &error) + { /* Method 1: "An administrator can create an $topdir/.Trash directory. The permissions on this @@ -1228,21 +1347,31 @@ static QString freeDesktopTrashLocation(const QString &sourcePath) (if it supports trashing in top directories) MUST check for the presence of $topdir/.Trash." */ - const QString userID = QString::number(::getuid()); - if (topDir.cd(dotTrash)) { - const QFileInfo trashInfo(topDir.path()); - // we MUST check that the sticky bit is set, and that it is not a symlink - if (trashInfo.isSymLink()) { + const auto dotTrash = "/.Trash"_L1; + const QString userID = QString::number(::getuid()); + 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()); + QT_STATBUF st = {}; + if (genericTrashFd < 0 && errno != ENOENT && errno != EACCES) { + // O_DIRECTORY + O_NOFOLLOW produces ENOTDIR on Linux + if (QT_LSTAT(dotTrashDir.nativeFilePath(), &st) == 0 && S_ISLNK(st.st_mode)) { // we SHOULD report the failed check to the administrator qCritical("Warning: '%s' is a symlink to '%s'", - trashInfo.absoluteFilePath().toLocal8Bit().constData(), - trashInfo.symLinkTarget().toLatin1().constData()); - } else if (!isSticky(trashInfo)) { + dotTrashDir.nativeFilePath().constData(), + qt_readlink(dotTrashDir.nativeFilePath()).constData()); + error = QSystemError(ELOOP, QSystemError::StandardLibraryError); + } + } else if (genericTrashFd >= 0) { + QT_FSTAT(genericTrashFd, &st); + if ((st.st_mode & S_ISVTX) == 0) { // we SHOULD report the failed check to the administrator qCritical("Warning: '%s' doesn't have sticky bit set!", - trashInfo.absoluteFilePath().toLocal8Bit().constData()); - } else if (trashInfo.isDir()) { + dotTrashDir.nativeFilePath().constData()); + error = QSystemError(EPERM, QSystemError::StandardLibraryError); + } else { /* "If the directory exists and passes the checks, a subdirectory of the $topdir/.Trash directory is to be used as the user's trash directory @@ -1252,9 +1381,14 @@ static QString freeDesktopTrashLocation(const QString &sourcePath) the implementation MUST immediately create it, without any warnings or delays for the user." */ - trash = makeTrashDir(topDir, userID); + if (getTrashDir(genericTrashFd, userID, source, error)) { + // recreate the resulting path + trashPath = dotTrashDir.filePath() + u'/' + userID; + } } + QT_CLOSE(genericTrashFd); } + /* Method 2: "If an $topdir/.Trash directory is absent, an $topdir/.Trash-$uid directory is to be @@ -1262,135 +1396,146 @@ static QString freeDesktopTrashLocation(const QString &sourcePath) file, if an $topdir/.Trash-$uid directory does not exist, the implementation MUST immediately create it, without any warnings or delays for the user." */ - if (trash.isEmpty()) { - topDir = QDir(sourceStorage.rootPath()); - const QString userTrashDir = dotTrash + u'-' + userID; - trash = makeTrashDir(topDir, userTrashDir); + if (!isTrashDirOpen()) + getTrashDir(AT_FDCWD, sourceStorage.rootPath() + dotTrash + u'-' + userID, source, error); + + if (isTrashDirOpen()) { + volumePrefixLength = sourceStorage.rootPath().size(); + if (volumePrefixLength == 1) + volumePrefixLength = 0; // isRoot + else + ++volumePrefixLength; // to include the slash } + return isTrashDirOpen(); } - /* - "If both (1) and (2) fail [...], the implementation MUST either trash the - file into the user's “home trash” or refuse to trash it." - We trash the file into the user's home trash. - - "Its name and location are $XDG_DATA_HOME/Trash"; $XDG_DATA_HOME is what - QStandardPaths returns for GenericDataLocation. If that doesn't exist, then - we are not running on a freedesktop.org-compliant environment, and give up. - */ - if (trash.isEmpty()) { - QDir topDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); - trash = makeTrashDir(topDir, "Trash"_L1); - if (!QFileInfo(trash).isDir()) { - qWarning("Unable to establish trash directory in %s", - topDir.path().toLocal8Bit().constData()); - } + bool openHomeTrashLocation(const QFileSystemEntry &source, QSystemError &error) + { + QString topDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + return getTrashDir(AT_FDCWD, topDir + "/Trash"_L1, source, error); } - return trash; -} -#endif // QT_BOOTSTRAPPED + bool findTrashFor(const QFileSystemEntry &source, QSystemError &error) + { + /* + First, try the standard Trash in $XDG_DATA_DIRS: + "Its name and location are $XDG_DATA_HOME/Trash"; $XDG_DATA_HOME is what + QStandardPaths returns for GenericDataLocation. If that doesn't exist, then + we are not running on a freedesktop.org-compliant environment, and give up. + */ + if (openHomeTrashLocation(source, error)) + return true; + if (error.errorCode != EXDEV) + return false; + + // didn't work, try to find the trash outside the home filesystem + const QStorageInfo sourceStorage(source.filePath()); + if (!sourceStorage.isValid()) + return false; + return openMountPointTrashLocation(source, sourceStorage, error); + } +}; +} // unnamed namespace //static bool QFileSystemEngine::moveFileToTrash(const QFileSystemEntry &source, QFileSystemEntry &newLocation, QSystemError &error) { -#ifdef QT_BOOTSTRAPPED - Q_UNUSED(source); - Q_UNUSED(newLocation); - error = QSystemError(ENOSYS, QSystemError::StandardLibraryError); - return false; -#else - const QFileInfo sourceInfo(source.filePath()); - if (!sourceInfo.exists()) { - error = QSystemError(ENOENT, QSystemError::StandardLibraryError); + const QFileSystemEntry sourcePath = [&] { + if (QString path = source.filePath(); path.size() > 1 && path.endsWith(u'/')) { + path.chop(1); + return absoluteName(QFileSystemEntry(path)); + } + return absoluteName(source); + }(); + FreeDesktopTrashOperation op; + if (!op.findTrashFor(sourcePath, error)) return false; - } - const QString sourcePath = sourceInfo.absoluteFilePath(); - QDir trashDir(freeDesktopTrashLocation(sourcePath)); - if (!trashDir.exists()) - return false; - /* - "A trash directory contains two subdirectories, named info and files." - */ - const auto filesDir = "files"_L1; - const auto infoDir = "info"_L1; - trashDir.mkdir(filesDir); - int savedErrno = errno; - trashDir.mkdir(infoDir); - if (!savedErrno) - savedErrno = errno; - if (!trashDir.exists(filesDir) || !trashDir.exists(infoDir)) { - error = QSystemError(savedErrno, QSystemError::StandardLibraryError); - return false; - } /* "The $trash/files directory contains the files and directories that were trashed. The names of files in this directory are to be determined by the implementation; the only limitation is that they must be unique within the directory. Even if a file with the same name and location gets trashed many times, each subsequent trashing must not overwrite a previous copy." - */ - const QString trashedName = sourceInfo.isDir() - ? QDir(sourcePath).dirName() - : sourceInfo.fileName(); - QString uniqueTrashedName = u'/' + trashedName; - QString infoFileName; - int counter = 0; - QFile infoFile; - auto makeUniqueTrashedName = [trashedName, &counter]() -> QString { - return QString::asprintf("/%ls-%04d", qUtf16Printable(trashedName), ++counter); - }; - do { - while (QFile::exists(trashDir.filePath(filesDir) + uniqueTrashedName)) - uniqueTrashedName = makeUniqueTrashedName(); - /* - "The $trash/info directory contains an "information file" for every file and directory - in $trash/files. This file MUST have exactly the same name as the file or directory in - $trash/files, plus the extension ".trashinfo" - [...] - When trashing a file or directory, the implementation MUST create the corresponding - file in $trash/info first. Moreover, it MUST try to do this in an atomic fashion, - so that if two processes try to trash files with the same filename this will result - in two different trash files. On Unix-like systems this is done by generating a - filename, and then opening with O_EXCL. If that succeeds the creation was atomic - (at least on the same machine), if it fails you need to pick another filename." - */ - infoFileName = trashDir.filePath(infoDir) - + uniqueTrashedName + ".trashinfo"_L1; - infoFile.setFileName(infoFileName); - if (!infoFile.open(QIODevice::NewOnly | QIODevice::WriteOnly | QIODevice::Text)) - uniqueTrashedName = makeUniqueTrashedName(); - } while (!infoFile.isOpen()); - - const QString targetPath = trashDir.filePath(filesDir) + uniqueTrashedName; - const QFileSystemEntry target(targetPath); - /* - We might fail to rename if source and target are on different file systems. - In that case, we don't try further, i.e. copying and removing the original - is usually not what the user would expect to happen. + We first try the unchanged base name, then try something different if it collides. + + "The $trash/info directory contains an "information file" for every file and directory + in $trash/files. This file MUST have exactly the same name as the file or directory in + $trash/files, plus the extension ".trashinfo" + [...] + When trashing a file or directory, the implementation MUST create the corresponding + file in $trash/info first. Moreover, it MUST try to do this in an atomic fashion, + so that if two processes try to trash files with the same filename this will result + in two different trash files. On Unix-like systems this is done by generating a + filename, and then opening with O_EXCL. If that succeeds the creation was atomic + (at least on the same machine), if it fails you need to pick another filename." */ - if (!renameFile(source, target, error)) { - infoFile.close(); - infoFile.remove(); - return false; + QString uniqueTrashedName = sourcePath.fileName(); + if (!op.tryCreateInfoFile(uniqueTrashedName, error) && error.errorCode == EEXIST) { + // we'll use a counter, starting with the file's inode number to avoid + // collisions + qulonglong counter; + if (QT_STATBUF st; Q_LIKELY(QT_STAT(source.nativeFilePath(), &st) == 0)) { + counter = st.st_ino; + } else { + error = QSystemError(errno, QSystemError::StandardLibraryError); + return false; + } + + QString uniqueTrashBase = std::move(uniqueTrashedName); + for (;;) { + uniqueTrashedName = QString::asprintf("%ls-%llu", qUtf16Printable(uniqueTrashBase), + counter++); + if (op.tryCreateInfoFile(uniqueTrashedName, error)) + break; + if (error.errorCode != EEXIST) + return false; + }; } QByteArray info = "[Trash Info]\n" - "Path=" + sourcePath.toUtf8() + "\n" - "DeletionDate=" + QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"_L1).toUtf8() + "Path=" + QUrl::toPercentEncoding(source.filePath().mid(op.volumePrefixLength), "/") + "\n" + "DeletionDate=" + QDateTime::currentDateTime().toString(Qt::ISODate).toUtf8() + "\n"; - infoFile.write(info); - infoFile.close(); + if (QT_WRITE(op.infoFileFd, info.data(), info.size()) < 0) { + error = QSystemError(errno, QSystemError::StandardLibraryError); + return false; + } - newLocation = QFileSystemEntry(targetPath); + /* + If we've already linked the file-to-be-trashed into the trash + directory, we know it's in the same mountpoint and we won't get ENOSPC + renaming the temporary file to the target name either. + */ + bool renamed; + if (op.tempTrashFileName.isEmpty()) { + /* + We did not get a link (we're trying to trash a directory or on a + filesystem that doesn't support hardlinking), so rename straight + from the original name. We might fail to rename if source and target + are on different file systems. + */ + renamed = renameat(AT_FDCWD, source.nativeFilePath(), op.filesDirFd, + QFile::encodeName(uniqueTrashedName)) == 0; + } else { + renamed = renameat(op.filesDirFd, op.tempTrashFileName, op.filesDirFd, + QFile::encodeName(uniqueTrashedName)) == 0; + if (renamed) + removeFile(source, error); // success, delete the original file + } + if (!renamed) { + error = QSystemError(errno, QSystemError::StandardLibraryError); + return false; + } + + op.commit(); + newLocation = QFileSystemEntry(op.trashPath + "/files/"_L1 + uniqueTrashedName); return true; -#endif // QT_BOOTSTRAPPED } -#endif // Q_OS_DARWIN +#endif // !Q_OS_DARWIN && !QT_BOOTSTRAPPED //static bool QFileSystemEngine::copyFile(const QFileSystemEntry &source, const QFileSystemEntry &target, QSystemError &error) @@ -1538,28 +1683,22 @@ bool QFileSystemEngine::setPermissions(int fd, QFile::Permissions permissions, Q } //static -bool QFileSystemEngine::setFileTime(int fd, const QDateTime &newDate, QAbstractFileEngine::FileTime time, QSystemError &error) +bool QFileSystemEngine::setFileTime(int fd, const QDateTime &newDate, QFile::FileTime time, QSystemError &error) { - if (!newDate.isValid() || time == QAbstractFileEngine::BirthTime || - time == QAbstractFileEngine::MetadataChangeTime) { + if (!newDate.isValid() + || time == QFile::FileBirthTime || time == QFile::FileMetadataChangeTime) { error = QSystemError(EINVAL, QSystemError::StandardLibraryError); return false; } #if QT_CONFIG(futimens) - struct timespec ts[2]; - - ts[0].tv_sec = ts[1].tv_sec = 0; - ts[0].tv_nsec = ts[1].tv_nsec = UTIME_OMIT; + // UTIME_OMIT: leave file timestamp unchanged + struct timespec ts[2] = {{0, UTIME_OMIT}, {0, UTIME_OMIT}}; - const qint64 msecs = newDate.toMSecsSinceEpoch(); - - if (time == QAbstractFileEngine::AccessTime) { - ts[0].tv_sec = msecs / 1000; - ts[0].tv_nsec = (msecs % 1000) * 1000000; - } else if (time == QAbstractFileEngine::ModificationTime) { - ts[1].tv_sec = msecs / 1000; - ts[1].tv_nsec = (msecs % 1000) * 1000000; + if (time == QFile::FileAccessTime || time == QFile::FileModificationTime) { + const int idx = time == QFile::FileAccessTime ? 0 : 1; + const std::chrono::milliseconds msecs{newDate.toMSecsSinceEpoch()}; + ts[idx] = durationToTimespec(msecs); } if (futimens(fd, ts) == -1) { @@ -1568,33 +1707,6 @@ bool QFileSystemEngine::setFileTime(int fd, const QDateTime &newDate, QAbstractF } return true; -#elif QT_CONFIG(futimes) - struct timeval tv[2]; - QT_STATBUF st; - - if (QT_FSTAT(fd, &st) == -1) { - error = QSystemError(errno, QSystemError::StandardLibraryError); - return false; - } - - GetFileTimes::get(&st, &tv[0], &tv[1]); - - const qint64 msecs = newDate.toMSecsSinceEpoch(); - - if (time == QAbstractFileEngine::AccessTime) { - tv[0].tv_sec = msecs / 1000; - tv[0].tv_usec = (msecs % 1000) * 1000; - } else if (time == QAbstractFileEngine::ModificationTime) { - tv[1].tv_sec = msecs / 1000; - tv[1].tv_usec = (msecs % 1000) * 1000; - } - - if (futimes(fd, tv) == -1) { - error = QSystemError(errno, QSystemError::StandardLibraryError); - return false; - } - - return true; #else Q_UNUSED(fd); error = QSystemError(ENOSYS, QSystemError::StandardLibraryError); |