diff options
author | Ahmad Samir <a.samirh78@gmail.com> | 2024-02-04 14:10:15 +0200 |
---|---|---|
committer | Ahmad Samir <a.samirh78@gmail.com> | 2024-02-29 16:35:57 +0200 |
commit | 78c33a77414585b39c2c3fa14d1c88d6af7c03f1 (patch) | |
tree | 33415f89d9bd3f5212b2ccd1ed116164fbc753e2 | |
parent | 6c424dbcb0385a4b36835fc5103ca74c1044eccc (diff) |
Copy QDirIterator.{cpp,h} to QDirListing.{cpp,h}
To make it easier to follow the history in git.
Change-Id: I094056c1ec130aeef77aa2d20289ab766bc25083
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
-rw-r--r-- | src/corelib/doc/snippets/code/src_corelib_io_qdirlisting.cpp | 28 | ||||
-rw-r--r-- | src/corelib/io/qdirlisting.cpp | 575 | ||||
-rw-r--r-- | src/corelib/io/qdirlisting.h | 55 | ||||
-rw-r--r-- | tests/auto/corelib/io/qdirlisting/CMakeLists.txt | 45 | ||||
-rw-r--r-- | tests/auto/corelib/io/qdirlisting/entrylist/directory/dummy | 0 | ||||
-rw-r--r-- | tests/auto/corelib/io/qdirlisting/entrylist/file | 0 | ||||
-rw-r--r-- | tests/auto/corelib/io/qdirlisting/tst_qdirlisting.cpp | 633 |
7 files changed, 1336 insertions, 0 deletions
diff --git a/src/corelib/doc/snippets/code/src_corelib_io_qdirlisting.cpp b/src/corelib/doc/snippets/code/src_corelib_io_qdirlisting.cpp new file mode 100644 index 0000000000..ec3f3adc50 --- /dev/null +++ b/src/corelib/doc/snippets/code/src_corelib_io_qdirlisting.cpp @@ -0,0 +1,28 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +//! [0] +QDirIterator it("/etc", QDirIterator::Subdirectories); +while (it.hasNext()) { + QString dir = it.next(); + qDebug() << dir; + // /etc/. + // /etc/.. + // /etc/X11 + // /etc/X11/fs + // ... +} +//! [0] + +//! [1] +QDirIterator it("/sys", QStringList() << "scaling_cur_freq", QDir::NoFilter, QDirIterator::Subdirectories); +while (it.hasNext()) { + QFile f(it.next()); + f.open(QIODevice::ReadOnly); + qDebug() << f.fileName() << f.readAll().trimmed().toDouble() / 1000 << "MHz"; +} +//! [1] + +//! [2] +QDirIterator audioFileIt(audioPath, {"*.mp3", "*.wav"}, QDir::Files); +//! [2] diff --git a/src/corelib/io/qdirlisting.cpp b/src/corelib/io/qdirlisting.cpp new file mode 100644 index 0000000000..aa725d03dd --- /dev/null +++ b/src/corelib/io/qdirlisting.cpp @@ -0,0 +1,575 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/*! + \since 4.3 + \class QDirIterator + \inmodule QtCore + \brief The QDirIterator class provides an iterator for directory entrylists. + + You can use QDirIterator to navigate entries of a directory one at a time. + It is similar to QDir::entryList() and QDir::entryInfoList(), but because + it lists entries one at a time instead of all at once, it scales better + and is more suitable for large directories. It also supports listing + directory contents recursively, and following symbolic links. Unlike + QDir::entryList(), QDirIterator does not support sorting. + + The QDirIterator constructor takes a QDir or a directory as + argument. After construction, the iterator is located before the first + directory entry. Here's how to iterate over all the entries sequentially: + + \snippet code/src_corelib_io_qdiriterator.cpp 0 + + Here's how to find and read all files filtered by name, recursively: + + \snippet code/src_corelib_io_qdiriterator.cpp 1 + + The next() and nextFileInfo() functions advance the iterator and return + the path or the QFileInfo of the next directory entry. You can also call + filePath() or fileInfo() to get the current file path or QFileInfo without + first advancing the iterator. The fileName() function returns only the + name of the file, similar to how QDir::entryList() works. + + Unlike Qt's container iterators, QDirIterator is uni-directional (i.e., + you cannot iterate directories in reverse order) and does not allow random + access. + + \sa QDir, QDir::entryList() +*/ + +/*! \enum QDirIterator::IteratorFlag + + This enum describes flags that you can combine to configure the behavior + of QDirIterator. + + \value NoIteratorFlags The default value, representing no flags. The + iterator will return entries for the assigned path. + + \value Subdirectories List entries inside all subdirectories as well. + + \value FollowSymlinks When combined with Subdirectories, this flag + enables iterating through all subdirectories of the assigned path, + following all symbolic links. Symbolic link loops (e.g., "link" => "." or + "link" => "..") are automatically detected and ignored. +*/ + +#include "qdiriterator.h" +#include "qdir_p.h" +#include "qabstractfileengine_p.h" + +#include <QtCore/qset.h> +#include <QtCore/qstack.h> +#include <QtCore/qvariant.h> +#if QT_CONFIG(regularexpression) +#include <QtCore/qregularexpression.h> +#endif + +#include <QtCore/private/qfilesystemiterator_p.h> +#include <QtCore/private/qfilesystementry_p.h> +#include <QtCore/private/qfilesystemmetadata_p.h> +#include <QtCore/private/qfilesystemengine_p.h> +#include <QtCore/private/qfileinfo_p.h> +#include <QtCore/private/qduplicatetracker_p.h> + +#include <memory> +#include <stack> +#include <vector> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +class QDirIteratorPrivate +{ +public: + void init(bool resolveEngine); + void advance(); + + bool entryMatches(const QString & fileName, const QFileInfo &fileInfo); + void pushDirectory(const QFileInfo &fileInfo); + void checkAndPushDirectory(const QFileInfo &); + bool matchesFilters(const QString &fileName, const QFileInfo &fi) const; + + std::unique_ptr<QAbstractFileEngine> engine; + + QFileSystemEntry dirEntry; + QStringList nameFilters; + QDir::Filters filters; + QDirIterator::IteratorFlags iteratorFlags; + +#if QT_CONFIG(regularexpression) + QList<QRegularExpression> nameRegExps; +#endif + + using FEngineIteratorPtr = std::unique_ptr<QAbstractFileEngineIterator>; + std::stack<FEngineIteratorPtr, std::vector<FEngineIteratorPtr>> fileEngineIterators; +#ifndef QT_NO_FILESYSTEMITERATOR + using FsIteratorPtr = std::unique_ptr<QFileSystemIterator>; + std::stack<FsIteratorPtr, std::vector<FsIteratorPtr>> nativeIterators; +#endif + + QFileInfo currentFileInfo; + QFileInfo nextFileInfo; + + // Loop protection + QDuplicateTracker<QString> visitedLinks; +}; + +void QDirIteratorPrivate::init(bool resolveEngine = true) +{ + if (nameFilters.contains("*"_L1)) + nameFilters.clear(); + + if (filters == QDir::NoFilter) + filters = QDir::AllEntries; + +#if QT_CONFIG(regularexpression) + nameRegExps.reserve(nameFilters.size()); + for (const auto &filter : nameFilters) { + auto re = QRegularExpression::fromWildcard(filter, (filters & QDir::CaseSensitive ? + Qt::CaseSensitive : Qt::CaseInsensitive)); + nameRegExps.append(re); + } +#endif + QFileSystemMetaData metaData; + if (resolveEngine) + engine.reset(QFileSystemEngine::resolveEntryAndCreateLegacyEngine(dirEntry, metaData)); + QFileInfo fileInfo(new QFileInfoPrivate(dirEntry, metaData)); + + // Populate fields for hasNext() and next() + pushDirectory(fileInfo); + advance(); +} + +/*! + \internal +*/ +void QDirIteratorPrivate::pushDirectory(const QFileInfo &fileInfo) +{ + QString path = fileInfo.filePath(); + +#ifdef Q_OS_WIN + if (fileInfo.isSymLink()) + path = fileInfo.canonicalFilePath(); +#endif + + if ((iteratorFlags & QDirIterator::FollowSymlinks)) { + // Stop link loops + if (visitedLinks.hasSeen(fileInfo.canonicalFilePath())) + return; + } + + if (engine) { + engine->setFileName(path); + QAbstractFileEngineIterator *it = engine->beginEntryList(filters, nameFilters); + if (it) { + it->setPath(path); + fileEngineIterators.emplace(FEngineIteratorPtr(it)); + } else { + // No iterator; no entry list. + } + } else { +#ifndef QT_NO_FILESYSTEMITERATOR + nativeIterators.emplace(std::make_unique<QFileSystemIterator>( + fileInfo.d_ptr->fileEntry, filters)); +#else + qWarning("Qt was built with -no-feature-filesystemiterator: no files/plugins will be found!"); +#endif + } +} + +inline bool QDirIteratorPrivate::entryMatches(const QString & fileName, const QFileInfo &fileInfo) +{ + checkAndPushDirectory(fileInfo); + + if (matchesFilters(fileName, fileInfo)) { + currentFileInfo = nextFileInfo; + nextFileInfo = fileInfo; + + //We found a matching entry. + return true; + } + + return false; +} + +/*! + \internal + + Advances the internal iterator, either a QAbstractFileEngineIterator (e.g. + QResourceFileEngineIterator) or a QFileSystemIterator (which uses low-level + system methods, e.g. readdir() on Unix). + + An iterator stack is used for holding the iterators. + + A typical example of doing recursive iteration: + - while iterating directory A we find a sub-dir B + - an iterator for B is added to the iterator stack + - B's iterator is processed (the top() of the stack) first; then loop + goes back to processing A's iterator +*/ +void QDirIteratorPrivate::advance() +{ + // Use get() in both code paths below because the iterator returned by top() + // may be invalidated due to reallocation when appending new iterators in + // pushDirectory(). + + if (engine) { + while (!fileEngineIterators.empty()) { + // Find the next valid iterator that matches the filters. + QAbstractFileEngineIterator *it; + while (it = fileEngineIterators.top().get(), it->hasNext()) { + it->next(); + if (entryMatches(it->currentFileName(), it->currentFileInfo())) + return; + } + + fileEngineIterators.pop(); + } + } else { +#ifndef QT_NO_FILESYSTEMITERATOR + QFileSystemEntry nextEntry; + QFileSystemMetaData nextMetaData; + + while (!nativeIterators.empty()) { + // Find the next valid iterator that matches the filters. + QFileSystemIterator *it; + while (it = nativeIterators.top().get(), it->advance(nextEntry, nextMetaData)) { + QFileInfo info(new QFileInfoPrivate(nextEntry, nextMetaData)); + + if (entryMatches(nextEntry.fileName(), info)) + return; + nextMetaData = QFileSystemMetaData(); + } + + nativeIterators.pop(); + } +#endif + } + + currentFileInfo = nextFileInfo; + nextFileInfo = QFileInfo(); +} + +/*! + \internal + */ +void QDirIteratorPrivate::checkAndPushDirectory(const QFileInfo &fileInfo) +{ + // If we're doing flat iteration, we're done. + if (!(iteratorFlags & QDirIterator::Subdirectories)) + return; + + // Never follow non-directory entries + if (!fileInfo.isDir()) + return; + + // Follow symlinks only when asked + if (!(iteratorFlags & QDirIterator::FollowSymlinks) && fileInfo.isSymLink()) + return; + + // Never follow . and .. + QString fileName = fileInfo.fileName(); + if ("."_L1 == fileName || ".."_L1 == fileName) + return; + + // No hidden directories unless requested + if (!(filters & QDir::AllDirs) && !(filters & QDir::Hidden) && fileInfo.isHidden()) + return; + + pushDirectory(fileInfo); +} + +/*! + \internal + + This convenience function implements the iterator's filtering logics and + applies then to the current directory entry. + + It returns \c true if the current entry matches the filters (i.e., the + current entry will be returned as part of the directory iteration); + otherwise, false is returned. +*/ + +bool QDirIteratorPrivate::matchesFilters(const QString &fileName, const QFileInfo &fi) const +{ + if (fileName.isEmpty()) + return false; + + // filter . and ..? + const qsizetype fileNameSize = fileName.size(); + const bool dotOrDotDot = fileName[0] == u'.' + && ((fileNameSize == 1) + ||(fileNameSize == 2 && fileName[1] == u'.')); + if ((filters & QDir::NoDot) && dotOrDotDot && fileNameSize == 1) + return false; + if ((filters & QDir::NoDotDot) && dotOrDotDot && fileNameSize == 2) + return false; + + // name filter +#if QT_CONFIG(regularexpression) + // Pass all entries through name filters, except dirs if the AllDirs + if (!nameFilters.isEmpty() && !((filters & QDir::AllDirs) && fi.isDir())) { + bool matched = false; + for (const auto &re : nameRegExps) { + if (re.match(fileName).hasMatch()) { + matched = true; + break; + } + } + if (!matched) + return false; + } +#endif + // skip symlinks + const bool skipSymlinks = filters.testAnyFlag(QDir::NoSymLinks); + const bool includeSystem = filters.testAnyFlag(QDir::System); + if (skipSymlinks && fi.isSymLink()) { + // The only reason to save this file is if it is a broken link and we are requesting system files. + if (!includeSystem || fi.exists()) + return false; + } + + // filter hidden + const bool includeHidden = filters.testAnyFlag(QDir::Hidden); + if (!includeHidden && !dotOrDotDot && fi.isHidden()) + return false; + + // filter system files + if (!includeSystem && (!(fi.isFile() || fi.isDir() || fi.isSymLink()) + || (!fi.exists() && fi.isSymLink()))) + return false; + + // skip directories + const bool skipDirs = !(filters & (QDir::Dirs | QDir::AllDirs)); + if (skipDirs && fi.isDir()) + return false; + + // skip files + const bool skipFiles = !(filters & QDir::Files); + if (skipFiles && fi.isFile()) + // Basically we need a reason not to exclude this file otherwise we just eliminate it. + return false; + + // filter permissions + const bool filterPermissions = ((filters & QDir::PermissionMask) + && (filters & QDir::PermissionMask) != QDir::PermissionMask); + const bool doWritable = !filterPermissions || (filters & QDir::Writable); + const bool doExecutable = !filterPermissions || (filters & QDir::Executable); + const bool doReadable = !filterPermissions || (filters & QDir::Readable); + if (filterPermissions + && ((doReadable && !fi.isReadable()) + || (doWritable && !fi.isWritable()) + || (doExecutable && !fi.isExecutable()))) { + return false; + } + + return true; +} + +/*! + Constructs a QDirIterator that can iterate over \a dir's entrylist, using + \a dir's name filters and regular filters. You can pass options via \a + flags to decide how the directory should be iterated. + + By default, \a flags is NoIteratorFlags, which provides the same behavior + as in QDir::entryList(). + + The sorting in \a dir is ignored. + + \note To list symlinks that point to non existing files, QDir::System must be + passed to the flags. + + \sa hasNext(), next(), IteratorFlags +*/ +QDirIterator::QDirIterator(const QDir &dir, IteratorFlags flags) + : d(new QDirIteratorPrivate) +{ + const QDirPrivate *other = dir.d_ptr.constData(); + d->dirEntry = other->dirEntry; + d->nameFilters = other->nameFilters; + d->filters = other->filters; + d->iteratorFlags = flags; + const bool resolveEngine = other->fileEngine ? true : false; + d->init(resolveEngine); +} + +/*! + Constructs a QDirIterator that can iterate over \a path, with no name + filtering and \a filters for entry filtering. You can pass options via \a + flags to decide how the directory should be iterated. + + By default, \a filters is QDir::NoFilter, and \a flags is NoIteratorFlags, + which provides the same behavior as in QDir::entryList(). + + \note To list symlinks that point to non existing files, QDir::System must be + passed to the flags. + + \sa hasNext(), next(), IteratorFlags +*/ +QDirIterator::QDirIterator(const QString &path, QDir::Filters filters, IteratorFlags flags) + : d(new QDirIteratorPrivate) +{ + d->dirEntry = QFileSystemEntry(path); + d->filters = filters; + d->iteratorFlags = flags; + d->init(); +} + +/*! + Constructs a QDirIterator that can iterate over \a path. You can pass + options via \a flags to decide how the directory should be iterated. + + By default, \a flags is NoIteratorFlags, which provides the same behavior + as in QDir::entryList(). + + \note To list symlinks that point to non existing files, QDir::System must be + passed to the flags. + + \sa hasNext(), next(), IteratorFlags +*/ +QDirIterator::QDirIterator(const QString &path, IteratorFlags flags) + : d(new QDirIteratorPrivate) +{ + d->dirEntry = QFileSystemEntry(path); + d->filters = QDir::NoFilter; + d->iteratorFlags = flags; + d->init(); +} + +/*! + Constructs a QDirIterator that can iterate over \a path, using \a + nameFilters and \a filters. You can pass options via \a flags to decide + how the directory should be iterated. + + By default, \a flags is NoIteratorFlags, which provides the same behavior + as QDir::entryList(). + + For example, the following iterator could be used to iterate over audio + files: + + \snippet code/src_corelib_io_qdiriterator.cpp 2 + + \note To list symlinks that point to non existing files, QDir::System must be + passed to the flags. + + \sa hasNext(), next(), IteratorFlags, QDir::setNameFilters() +*/ +QDirIterator::QDirIterator(const QString &path, const QStringList &nameFilters, + QDir::Filters filters, IteratorFlags flags) + : d(new QDirIteratorPrivate) +{ + d->dirEntry = QFileSystemEntry(path); + d->nameFilters = nameFilters; + d->filters = filters; + d->iteratorFlags = flags; + d->init(); +} + +/*! + Destroys the QDirIterator. +*/ +QDirIterator::~QDirIterator() +{ +} + +/*! + Advances the iterator to the next entry, and returns the file path of this + new entry. If hasNext() returns \c false, this function does nothing, and + returns an empty QString. + + You can call fileName() or filePath() to get the current entry's file name + or path, or fileInfo() to get a QFileInfo for the current entry. + + Call nextFileInfo() instead of next() if you're interested in the QFileInfo. + + \sa hasNext(), nextFileInfo(), fileName(), filePath(), fileInfo() +*/ +QString QDirIterator::next() +{ + d->advance(); + return filePath(); +} + +/*! + \since 6.3 + + Advances the iterator to the next entry, and returns the file info of this + new entry. If hasNext() returns \c false, this function does nothing, and + returns an empty QFileInfo. + + You can call fileName() or filePath() to get the current entry's file name + or path, or fileInfo() to get a QFileInfo for the current entry. + + Call next() instead of nextFileInfo() when all you need is the filePath(). + + \sa hasNext(), fileName(), filePath(), fileInfo() +*/ +QFileInfo QDirIterator::nextFileInfo() +{ + d->advance(); + return fileInfo(); +} + +/*! + Returns \c true if there is at least one more entry in the directory; + otherwise, false is returned. + + \sa next(), nextFileInfo(), fileName(), filePath(), fileInfo() +*/ +bool QDirIterator::hasNext() const +{ + if (d->engine) + return !d->fileEngineIterators.empty(); + else +#ifndef QT_NO_FILESYSTEMITERATOR + return !d->nativeIterators.empty(); +#else + return false; +#endif +} + +/*! + Returns the file name for the current directory entry, without the path + prepended. + + This function is convenient when iterating a single directory. When using + the QDirIterator::Subdirectories flag, you can use filePath() to get the + full path. + + \sa filePath(), fileInfo() +*/ +QString QDirIterator::fileName() const +{ + return d->currentFileInfo.fileName(); +} + +/*! + Returns the full file path for the current directory entry. + + \sa fileInfo(), fileName() +*/ +QString QDirIterator::filePath() const +{ + return d->currentFileInfo.filePath(); +} + +/*! + Returns a QFileInfo for the current directory entry. + + \sa filePath(), fileName() +*/ +QFileInfo QDirIterator::fileInfo() const +{ + return d->currentFileInfo; +} + +/*! + Returns the base directory of the iterator. +*/ +QString QDirIterator::path() const +{ + return d->dirEntry.filePath(); +} + +QT_END_NAMESPACE diff --git a/src/corelib/io/qdirlisting.h b/src/corelib/io/qdirlisting.h new file mode 100644 index 0000000000..7fa612ebe0 --- /dev/null +++ b/src/corelib/io/qdirlisting.h @@ -0,0 +1,55 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDIRITERATOR_H +#define QDIRITERATOR_H + +#include <QtCore/qdir.h> + +QT_BEGIN_NAMESPACE + +class QDirIteratorPrivate; +class Q_CORE_EXPORT QDirIterator +{ +public: + enum IteratorFlag { + NoIteratorFlags = 0x0, + FollowSymlinks = 0x1, + Subdirectories = 0x2 + }; + Q_DECLARE_FLAGS(IteratorFlags, IteratorFlag) + + QDirIterator(const QDir &dir, IteratorFlags flags = NoIteratorFlags); + QDirIterator(const QString &path, + IteratorFlags flags = NoIteratorFlags); + QDirIterator(const QString &path, + QDir::Filters filter, + IteratorFlags flags = NoIteratorFlags); + QDirIterator(const QString &path, + const QStringList &nameFilters, + QDir::Filters filters = QDir::NoFilter, + IteratorFlags flags = NoIteratorFlags); + + ~QDirIterator(); + + QString next(); + QFileInfo nextFileInfo(); + bool hasNext() const; + + QString fileName() const; + QString filePath() const; + QFileInfo fileInfo() const; + QString path() const; + +private: + Q_DISABLE_COPY(QDirIterator) + + QScopedPointer<QDirIteratorPrivate> d; + friend class QDir; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QDirIterator::IteratorFlags) + +QT_END_NAMESPACE + +#endif diff --git a/tests/auto/corelib/io/qdirlisting/CMakeLists.txt b/tests/auto/corelib/io/qdirlisting/CMakeLists.txt new file mode 100644 index 0000000000..41784546aa --- /dev/null +++ b/tests/auto/corelib/io/qdirlisting/CMakeLists.txt @@ -0,0 +1,45 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qdiriterator Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qdiriterator LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +# Collect test data +list(APPEND test_data "entrylist") + +qt_internal_add_test(tst_qdiriterator + SOURCES + tst_qdiriterator.cpp + LIBRARIES + Qt::CorePrivate + TESTDATA ${test_data} +) + +# Resources: +set(qdiriterator_resource_files + "entrylist/directory/dummy" + "entrylist/file" +) + +qt_internal_add_resource(tst_qdiriterator "qdiriterator" + PREFIX + "/testdata/" + FILES + ${qdiriterator_resource_files} +) + + +## Scopes: +##################################################################### + +qt_internal_extend_target(tst_qdiriterator CONDITION CONFIG___contains___builtin_testdata + DEFINES + BUILTIN_TESTDATA +) diff --git a/tests/auto/corelib/io/qdirlisting/entrylist/directory/dummy b/tests/auto/corelib/io/qdirlisting/entrylist/directory/dummy new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/auto/corelib/io/qdirlisting/entrylist/directory/dummy diff --git a/tests/auto/corelib/io/qdirlisting/entrylist/file b/tests/auto/corelib/io/qdirlisting/entrylist/file new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/auto/corelib/io/qdirlisting/entrylist/file diff --git a/tests/auto/corelib/io/qdirlisting/tst_qdirlisting.cpp b/tests/auto/corelib/io/qdirlisting/tst_qdirlisting.cpp new file mode 100644 index 0000000000..ac92b9b692 --- /dev/null +++ b/tests/auto/corelib/io/qdirlisting/tst_qdirlisting.cpp @@ -0,0 +1,633 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QTest> + +#include <qcoreapplication.h> +#include <qdebug.h> +#include <qdiriterator.h> +#include <qfileinfo.h> +#include <qstringlist.h> +#include <QSet> +#include <QString> + +#include <QtCore/private/qfsfileengine_p.h> + +#if defined(Q_OS_VXWORKS) +#define Q_NO_SYMLINKS +#endif + +#include "../../../../shared/filesystem.h" + +#ifdef Q_OS_ANDROID +#include <QStandardPaths> +#endif + +using namespace Qt::StringLiterals; + +Q_DECLARE_METATYPE(QDirIterator::IteratorFlags) +Q_DECLARE_METATYPE(QDir::Filters) + +class tst_QDirIterator : public QObject +{ + Q_OBJECT + +private: // convenience functions + QStringList createdDirectories; + QStringList createdFiles; + + QDir currentDir; + bool createDirectory(const QString &dirName) + { + if (currentDir.mkdir(dirName)) { + createdDirectories.prepend(dirName); + return true; + } + return false; + } + + bool createFile(const QString &fileName) + { + QFile file(fileName); + return file.open(QIODevice::WriteOnly); + } + + bool createLink(const QString &destination, const QString &linkName) + { + if (QFile::link(destination, linkName)) { + createdFiles << linkName; + return true; + } + return false; + } + +private slots: + void initTestCase(); + void iterateRelativeDirectory_data(); + void iterateRelativeDirectory(); + void iterateResource_data(); + void iterateResource(); + void stopLinkLoop(); +#ifdef QT_BUILD_INTERNAL + void engineWithNoIterator(); + void testQFsFileEngineIterator_data() { iterateRelativeDirectory_data(); } + void testQFsFileEngineIterator(); +#endif + void absoluteFilePathsFromRelativeIteratorPath(); + void recurseWithFilters() const; + void longPath(); + void dirorder(); + void relativePaths(); +#if defined(Q_OS_WIN) + void uncPaths_data(); + void uncPaths(); +#endif +#ifndef Q_OS_WIN + void hiddenDirs_hiddenFiles(); +#endif + +private: + QTemporaryDir m_dataDir; +}; + +void tst_QDirIterator::initTestCase() +{ +#ifdef Q_OS_ANDROID + QString testdata_dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + QString resourceSourcePath = QStringLiteral(":/testdata"); + QDirIterator it(resourceSourcePath, QDirIterator::Subdirectories); + while (it.hasNext()) { + QFileInfo fileInfo = it.nextFileInfo(); + + if (!fileInfo.isDir()) { + QString destination = testdata_dir + QLatin1Char('/') + + fileInfo.filePath().mid(resourceSourcePath.length()); + QFileInfo destinationFileInfo(destination); + if (!destinationFileInfo.exists()) { + QDir().mkpath(destinationFileInfo.path()); + if (!QFile::copy(fileInfo.filePath(), destination)) + qWarning("Failed to copy %s", qPrintable(fileInfo.filePath())); + } + } + + } + + testdata_dir += QStringLiteral("/entrylist"); +#elif defined(BUILTIN_TESTDATA) + m_dataDir = QEXTRACTTESTDATA("/testdata"); + QVERIFY2(!m_dataDir.isNull(), qPrintable("Could not extract test data")); + QString testdata_dir = m_dataDir->path(); +#else + + // chdir into testdata directory, then find testdata by relative paths. + QString testdata_dir = m_dataDir.path(); +#endif + + QVERIFY(!testdata_dir.isEmpty()); + // Must call QDir::setCurrent() here because all the tests that use relative + // paths depend on that. + QVERIFY2(QDir::setCurrent(testdata_dir), qPrintable("Could not chdir to " + testdata_dir)); + + createDirectory("entrylist"); + createDirectory("entrylist/directory"); + createFile("entrylist/file"); + createFile("entrylist/writable"); + createFile("entrylist/directory/dummy"); + + createDirectory("recursiveDirs"); + createDirectory("recursiveDirs/dir1"); + createFile("recursiveDirs/textFileA.txt"); + createFile("recursiveDirs/dir1/aPage.html"); + createFile("recursiveDirs/dir1/textFileB.txt"); + + createDirectory("foo"); + createDirectory("foo/bar"); + createFile("foo/bar/readme.txt"); + + createDirectory("empty"); + +#ifndef Q_NO_SYMLINKS +# if defined(Q_OS_WIN) + // ### Sadly, this is a platform difference right now. + createLink("entrylist/file", "entrylist/linktofile.lnk"); +# ifndef Q_NO_SYMLINKS_TO_DIRS + createLink("entrylist/directory", "entrylist/linktodirectory.lnk"); +# endif + createLink("entrylist/nothing", "entrylist/brokenlink.lnk"); +# else + createLink("file", "entrylist/linktofile.lnk"); +# ifndef Q_NO_SYMLINKS_TO_DIRS + createLink("directory", "entrylist/linktodirectory.lnk"); +# endif + createLink("nothing", "entrylist/brokenlink.lnk"); +# endif +#endif + +#if !defined(Q_OS_WIN) + createDirectory("hiddenDirs_hiddenFiles"); + createFile("hiddenDirs_hiddenFiles/normalFile"); + createFile("hiddenDirs_hiddenFiles/.hiddenFile"); + createDirectory("hiddenDirs_hiddenFiles/normalDirectory"); + createDirectory("hiddenDirs_hiddenFiles/.hiddenDirectory"); + createFile("hiddenDirs_hiddenFiles/normalDirectory/normalFile"); + createFile("hiddenDirs_hiddenFiles/normalDirectory/.hiddenFile"); + createFile("hiddenDirs_hiddenFiles/.hiddenDirectory/normalFile"); + createFile("hiddenDirs_hiddenFiles/.hiddenDirectory/.hiddenFile"); + createDirectory("hiddenDirs_hiddenFiles/normalDirectory/normalDirectory"); + createDirectory("hiddenDirs_hiddenFiles/normalDirectory/.hiddenDirectory"); + createDirectory("hiddenDirs_hiddenFiles/.hiddenDirectory/normalDirectory"); + createDirectory("hiddenDirs_hiddenFiles/.hiddenDirectory/.hiddenDirectory"); +#endif +} + +void tst_QDirIterator::iterateRelativeDirectory_data() +{ + QTest::addColumn<QString>("dirName"); // relative from current path or abs + QTest::addColumn<QDirIterator::IteratorFlags>("flags"); + QTest::addColumn<QDir::Filters>("filters"); + QTest::addColumn<QStringList>("nameFilters"); + QTest::addColumn<QStringList>("entries"); + + QTest::newRow("no flags") + << QString("entrylist") << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::NoFilter) << QStringList("*") + << QString( + "entrylist/.," + "entrylist/..," + "entrylist/file," +#ifndef Q_NO_SYMLINKS + "entrylist/linktofile.lnk," +#endif + "entrylist/directory," +#if !defined(Q_NO_SYMLINKS) && !defined(Q_NO_SYMLINKS_TO_DIRS) + "entrylist/linktodirectory.lnk," +#endif + "entrylist/writable").split(','); + + QTest::newRow("NoDot") + << QString("entrylist") << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::AllEntries | QDir::NoDot) << QStringList("*") + << QString( + "entrylist/..," + "entrylist/file," +#ifndef Q_NO_SYMLINKS + "entrylist/linktofile.lnk," +#endif + "entrylist/directory," +#if !defined(Q_NO_SYMLINKS) && !defined(Q_NO_SYMLINKS_TO_DIRS) + "entrylist/linktodirectory.lnk," +#endif + "entrylist/writable").split(','); + + QTest::newRow("NoDotDot") + << QString("entrylist") << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::AllEntries | QDir::NoDotDot) << QStringList("*") + << QString( + "entrylist/.," + "entrylist/file," +#ifndef Q_NO_SYMLINKS + "entrylist/linktofile.lnk," +#endif + "entrylist/directory," +#if !defined(Q_NO_SYMLINKS) && !defined(Q_NO_SYMLINKS_TO_DIRS) + "entrylist/linktodirectory.lnk," +#endif + "entrylist/writable").split(','); + + QTest::newRow("NoDotAndDotDot") + << QString("entrylist") << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::AllEntries | QDir::NoDotAndDotDot) << QStringList("*") + << QString( + "entrylist/file," +#ifndef Q_NO_SYMLINKS + "entrylist/linktofile.lnk," +#endif + "entrylist/directory," +#if !defined(Q_NO_SYMLINKS) && !defined(Q_NO_SYMLINKS_TO_DIRS) + "entrylist/linktodirectory.lnk," +#endif + "entrylist/writable").split(','); + + QTest::newRow("QDir::Subdirectories | QDir::FollowSymlinks") + << QString("entrylist") << QDirIterator::IteratorFlags(QDirIterator::Subdirectories | QDirIterator::FollowSymlinks) + << QDir::Filters(QDir::NoFilter) << QStringList("*") + << QString( + "entrylist/.," + "entrylist/..," + "entrylist/directory/.," + "entrylist/directory/..," + "entrylist/file," +#ifndef Q_NO_SYMLINKS + "entrylist/linktofile.lnk," +#endif + "entrylist/directory," + "entrylist/directory/dummy," +#if !defined(Q_NO_SYMLINKS) && !defined(Q_NO_SYMLINKS_TO_DIRS) + "entrylist/linktodirectory.lnk," +#endif + "entrylist/writable").split(','); + + QTest::newRow("QDir::Subdirectories / QDir::Files") + << QString("entrylist") << QDirIterator::IteratorFlags(QDirIterator::Subdirectories) + << QDir::Filters(QDir::Files) << QStringList("*") + << QString("entrylist/directory/dummy," + "entrylist/file," +#ifndef Q_NO_SYMLINKS + "entrylist/linktofile.lnk," +#endif + "entrylist/writable").split(','); + + QTest::newRow("QDir::Subdirectories | QDir::FollowSymlinks / QDir::Files") + << QString("entrylist") << QDirIterator::IteratorFlags(QDirIterator::Subdirectories | QDirIterator::FollowSymlinks) + << QDir::Filters(QDir::Files) << QStringList("*") + << QString("entrylist/file," +#ifndef Q_NO_SYMLINKS + "entrylist/linktofile.lnk," +#endif + "entrylist/directory/dummy," + "entrylist/writable").split(','); + + QTest::newRow("empty, default") + << QString("empty") << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::NoFilter) << QStringList("*") + << QString("empty/.,empty/..").split(','); + + QTest::newRow("empty, QDir::NoDotAndDotDot") + << QString("empty") << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::NoDotAndDotDot) << QStringList("*") + << QStringList(); +} + +void tst_QDirIterator::iterateRelativeDirectory() +{ + QFETCH(QString, dirName); + QFETCH(QDirIterator::IteratorFlags, flags); + QFETCH(QDir::Filters, filters); + QFETCH(QStringList, nameFilters); + QFETCH(const QStringList, entries); + + QDirIterator it(dirName, nameFilters, filters, flags); + QStringList list; + while (it.hasNext()) { + QString next = it.next(); + + QString fileName = it.fileName(); + QString filePath = it.filePath(); + QString path = it.path(); + + QFileInfo info = it.fileInfo(); + + QCOMPARE(path, dirName); + QCOMPARE(next, filePath); + + QCOMPARE(info, QFileInfo(next)); + QCOMPARE(fileName, info.fileName()); + QCOMPARE(filePath, info.filePath()); + + // Using canonical file paths for final comparison + list << info.canonicalFilePath(); + } + + // The order of items returned by QDirIterator is not guaranteed. + list.sort(); + + QStringList sortedEntries; + for (const QString &item : entries) + sortedEntries.append(QFileInfo(item).canonicalFilePath()); + sortedEntries.sort(); + + if (sortedEntries != list) { + qDebug() << "ACTUAL: " << list; + qDebug() << "EXPECTED:" << sortedEntries; + } + + QCOMPARE(list, sortedEntries); +} + +void tst_QDirIterator::iterateResource_data() +{ + QTest::addColumn<QString>("dirName"); // relative from current path or abs + QTest::addColumn<QDirIterator::IteratorFlags>("flags"); + QTest::addColumn<QDir::Filters>("filters"); + QTest::addColumn<QStringList>("nameFilters"); + QTest::addColumn<QStringList>("entries"); + + QTest::newRow("invalid") << QString::fromLatin1(":/testdata/burpaburpa") << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::NoFilter) << QStringList(QLatin1String("*")) + << QStringList(); + QTest::newRow("qrc:/testdata") << u":/testdata/"_s << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::NoFilter) << QStringList(QLatin1String("*")) + << QString::fromLatin1(":/testdata/entrylist").split(QLatin1String(",")); + QTest::newRow("qrc:/testdata/entrylist") << u":/testdata/entrylist"_s << QDirIterator::IteratorFlags{} + << QDir::Filters(QDir::NoFilter) << QStringList(QLatin1String("*")) + << QString::fromLatin1(":/testdata/entrylist/directory,:/testdata/entrylist/file").split(QLatin1String(",")); + QTest::newRow("qrc:/testdata recursive") << u":/testdata"_s + << QDirIterator::IteratorFlags(QDirIterator::Subdirectories) + << QDir::Filters(QDir::NoFilter) << QStringList(QLatin1String("*")) + << QString::fromLatin1(":/testdata/entrylist,:/testdata/entrylist/directory,:/testdata/entrylist/directory/dummy,:/testdata/entrylist/file").split(QLatin1String(",")); +} + +void tst_QDirIterator::iterateResource() +{ + QFETCH(QString, dirName); + QFETCH(QDirIterator::IteratorFlags, flags); + QFETCH(QDir::Filters, filters); + QFETCH(QStringList, nameFilters); + QFETCH(QStringList, entries); + + QDirIterator it(dirName, nameFilters, filters, flags); + QStringList list; + while (it.hasNext()) { + const QString dir = it.next(); + if (!dir.startsWith(":/qt-project.org")) + list << dir; + } + + list.sort(); + QStringList sortedEntries = entries; + sortedEntries.sort(); + + if (sortedEntries != list) { + qDebug() << "ACTUAL:" << list; + qDebug() << "EXPECTED:" << sortedEntries; + } + + QCOMPARE(list, sortedEntries); +} + +void tst_QDirIterator::stopLinkLoop() +{ +#ifdef Q_OS_WIN + // ### Sadly, this is a platform difference right now. + createLink(QDir::currentPath() + QLatin1String("/entrylist"), "entrylist/entrylist1.lnk"); + createLink("entrylist/.", "entrylist/entrylist2.lnk"); + createLink("entrylist/../entrylist/.", "entrylist/entrylist3.lnk"); + createLink("entrylist/..", "entrylist/entrylist4.lnk"); + createLink(QDir::currentPath() + QLatin1String("/entrylist"), "entrylist/directory/entrylist1.lnk"); + createLink("entrylist/.", "entrylist/directory/entrylist2.lnk"); + createLink("entrylist/../directory/.", "entrylist/directory/entrylist3.lnk"); + createLink("entrylist/..", "entrylist/directory/entrylist4.lnk"); +#else + createLink(QDir::currentPath() + QLatin1String("/entrylist"), "entrylist/entrylist1.lnk"); + createLink(".", "entrylist/entrylist2.lnk"); + createLink("../entrylist/.", "entrylist/entrylist3.lnk"); + createLink("..", "entrylist/entrylist4.lnk"); + createLink(QDir::currentPath() + QLatin1String("/entrylist"), "entrylist/directory/entrylist1.lnk"); + createLink(".", "entrylist/directory/entrylist2.lnk"); + createLink("../directory/.", "entrylist/directory/entrylist3.lnk"); + createLink("..", "entrylist/directory/entrylist4.lnk"); +#endif + + QDirIterator it(QLatin1String("entrylist"), QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + QStringList list; + int max = 200; + while (--max && it.hasNext()) + it.nextFileInfo(); + QVERIFY(max); + + // The goal of this test is only to ensure that the test above don't malfunction +} + +#ifdef QT_BUILD_INTERNAL +class EngineWithNoIterator : public QFSFileEngine +{ +public: + EngineWithNoIterator(const QString &fileName) + : QFSFileEngine(fileName) + { } + + QAbstractFileEngineIterator *beginEntryList(QDir::Filters, const QStringList &) override + { return 0; } +}; + +class EngineWithNoIteratorHandler : public QAbstractFileEngineHandler +{ +public: + QAbstractFileEngine *create(const QString &fileName) const override + { + return new EngineWithNoIterator(fileName); + } +}; +#endif + +#ifdef QT_BUILD_INTERNAL +void tst_QDirIterator::engineWithNoIterator() +{ + EngineWithNoIteratorHandler handler; + + QDir("entrylist").entryList(); + QVERIFY(true); // test that the above line doesn't crash +} + +class CustomEngineHandler : public QAbstractFileEngineHandler +{ +public: + QAbstractFileEngine *create(const QString &fileName) const override + { + // We want to test QFSFileEngine specifically, so force QDirIterator to use it + // over the default QFileSystemEngine + return new QFSFileEngine(fileName); + } +}; + +void tst_QDirIterator::testQFsFileEngineIterator() +{ + QFETCH(QString, dirName); + QFETCH(QStringList, nameFilters); + QFETCH(QDir::Filters, filters); + QFETCH(QDirIterator::IteratorFlags, flags); + + if (dirName == u"empty") + return; // This row isn't useful in this test + + CustomEngineHandler handler; + bool isEmpty = true; + QDirIterator iter(dirName, nameFilters, filters, flags); + while (iter.hasNext()) { + const QFileInfo &fi = iter.nextFileInfo(); + if (fi.filePath().contains(u"entrylist")) + isEmpty = false; // At least one entry in `entrylist` dir + } + QVERIFY(!isEmpty); +} +#endif + +void tst_QDirIterator::absoluteFilePathsFromRelativeIteratorPath() +{ + QDirIterator it("entrylist/", QDir::NoDotAndDotDot); + while (it.hasNext()) + QVERIFY(it.nextFileInfo().absoluteFilePath().contains("entrylist")); +} + +void tst_QDirIterator::recurseWithFilters() const +{ + QStringList nameFilters; + nameFilters.append("*.txt"); + + QDirIterator it("recursiveDirs/", nameFilters, QDir::Files, + QDirIterator::Subdirectories); + + QSet<QString> actualEntries; + QSet<QString> expectedEntries; + expectedEntries.insert(QString::fromLatin1("recursiveDirs/dir1/textFileB.txt")); + expectedEntries.insert(QString::fromLatin1("recursiveDirs/textFileA.txt")); + + QVERIFY(it.hasNext()); + actualEntries.insert(it.next()); + QVERIFY(it.hasNext()); + actualEntries.insert(it.next()); + QCOMPARE(actualEntries, expectedEntries); + + QVERIFY(!it.hasNext()); +} + +void tst_QDirIterator::longPath() +{ + QDir dir; + dir.mkdir("longpaths"); + dir.cd("longpaths"); + + QString dirName = "x"; + int n = 0; + while (dir.exists(dirName) || dir.mkdir(dirName)) { + ++n; + dirName.append('x'); + } + + QDirIterator it(dir.absolutePath(), QDir::NoDotAndDotDot|QDir::Dirs, QDirIterator::Subdirectories); + int m = 0; + while (it.hasNext()) { + ++m; + it.nextFileInfo(); + } + + QCOMPARE(n, m); + + dirName.chop(1); + while (dirName.size() > 0 && dir.exists(dirName) && dir.rmdir(dirName)) { + dirName.chop(1); + } + dir.cdUp(); + dir.rmdir("longpaths"); +} + +void tst_QDirIterator::dirorder() +{ + QDirIterator iterator("foo", QDirIterator::Subdirectories); + while (iterator.hasNext() && iterator.next() != "foo/bar") + { } + + QCOMPARE(iterator.filePath(), QString("foo/bar")); + QCOMPARE(iterator.fileInfo().filePath(), QString("foo/bar")); +} + +void tst_QDirIterator::relativePaths() +{ + QDirIterator iterator("*", QDirIterator::Subdirectories); + while(iterator.hasNext()) { + QCOMPARE(iterator.filePath(), QDir::cleanPath(iterator.filePath())); + } +} + +#if defined(Q_OS_WIN) +void tst_QDirIterator::uncPaths_data() +{ + QTest::addColumn<QString>("dirName"); + QTest::newRow("uncserver") + <<QString("//" + QTest::uncServerName()); + QTest::newRow("uncserver/testshare") + <<QString("//" + QTest::uncServerName() + "/testshare"); + QTest::newRow("uncserver/testshare/tmp") + <<QString("//" + QTest::uncServerName() + "/testshare/tmp"); +} +void tst_QDirIterator::uncPaths() +{ + QFETCH(QString, dirName); + QDirIterator iterator(dirName, QDir::AllEntries|QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while(iterator.hasNext()) { + iterator.next(); + QCOMPARE(iterator.filePath(), QDir::cleanPath(iterator.filePath())); + } +} +#endif + +#ifndef Q_OS_WIN +// In Unix it is easy to create hidden files, but in Windows it requires +// a special call since hidden files need to be "marked" while in Unix +// anything starting by a '.' is a hidden file. +// For that reason this test is not run in Windows. +void tst_QDirIterator::hiddenDirs_hiddenFiles() +{ + // Only files + { + int matches = 0; + int failures = 0; + QDirIterator di("hiddenDirs_hiddenFiles", QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (di.hasNext()) { + ++matches; + if (di.nextFileInfo().isDir()) + ++failures; // search was only supposed to find files + } + QCOMPARE(matches, 6); + QCOMPARE(failures, 0); + } + // Only directories + { + int matches = 0; + int failures = 0; + QDirIterator di("hiddenDirs_hiddenFiles", QDir::Dirs | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (di.hasNext()) { + ++matches; + if (!di.nextFileInfo().isDir()) + ++failures; // search was only supposed to find files + } + QCOMPARE(matches, 6); + QCOMPARE(failures, 0); + } +} +#endif // Q_OS_WIN + +QTEST_MAIN(tst_QDirIterator) + +#include "tst_qdiriterator.moc" + |