aboutsummaryrefslogtreecommitdiff
path: root/libs/uibase/src/ifiletree.cpp
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/uibase/src/ifiletree.cpp
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/uibase/src/ifiletree.cpp')
-rw-r--r--libs/uibase/src/ifiletree.cpp1029
1 files changed, 1029 insertions, 0 deletions
diff --git a/libs/uibase/src/ifiletree.cpp b/libs/uibase/src/ifiletree.cpp
new file mode 100644
index 0000000..b582d18
--- /dev/null
+++ b/libs/uibase/src/ifiletree.cpp
@@ -0,0 +1,1029 @@
+#include <uibase/ifiletree.h>
+
+#include <algorithm>
+#include <ranges>
+#include <span>
+#include <stack>
+
+#include <QRegularExpression>
+
+// FileTreeEntry:
+namespace MOBase
+{
+FileTreeEntry::FileTreeEntry(std::shared_ptr<const IFileTree> parent, QString name)
+ : m_Parent(parent), m_Name(name)
+{}
+
+QString FileTreeEntry::suffix() const
+{
+ const qsizetype idx = m_Name.lastIndexOf(".");
+ return (isDir() || idx == -1) ? "" : m_Name.mid(idx + 1);
+}
+
+bool FileTreeEntry::hasSuffix(QString suffix) const
+{
+ return this->suffix().compare(suffix, FileNameComparator::CaseSensitivity) == 0;
+}
+
+bool FileTreeEntry::hasSuffix(QStringList suffixes) const
+{
+ return suffixes.contains(suffix(), FileNameComparator::CaseSensitivity);
+}
+
+QString FileTreeEntry::pathFrom(std::shared_ptr<const IFileTree> tree,
+ QString sep) const
+{
+
+ // We will construct the path from right to left:
+ QString path = name();
+
+ auto p = parent();
+ while (p != nullptr && p != tree) {
+ // We need to check the parent, otherwize we are going to prepend the name
+ // and a / for the base, which we do not want.
+ if (p->parent() != nullptr) {
+ path = p->name() + sep + path;
+ }
+ p = p->parent();
+ }
+
+ return p == tree ? path : QString();
+}
+
+bool FileTreeEntry::detach()
+{
+ auto p = parent();
+ if (p == nullptr) {
+ return false;
+ }
+ return p->erase(shared_from_this()) != p->end();
+}
+
+bool FileTreeEntry::moveTo(std::shared_ptr<IFileTree> tree)
+{
+ return tree->insert(shared_from_this()) != tree->end();
+}
+
+std::shared_ptr<FileTreeEntry> FileTreeEntry::clone() const
+{
+ return createFileEntry(nullptr, name());
+}
+
+std::shared_ptr<FileTreeEntry>
+FileTreeEntry::createFileEntry(std::shared_ptr<const IFileTree> parent, QString name)
+{
+ return std::shared_ptr<FileTreeEntry>(new FileTreeEntry(parent, name));
+}
+} // namespace MOBase
+
+// IFileTree:
+namespace MOBase
+{
+
+/**
+ * Comparator for file entries.
+ */
+struct FileEntryComparator
+{
+ bool operator()(std::shared_ptr<FileTreeEntry> const& a,
+ std::shared_ptr<FileTreeEntry> const& b) const
+ {
+ if (a->isDir() && !b->isDir()) {
+ return true;
+ } else if (!a->isDir() && b->isDir()) {
+ return false;
+ } else {
+ return FileNameComparator::compare(a->name(), b->name()) < 0;
+ }
+ }
+};
+
+/**
+ * @brief Comparator that can be used to find entry matching the given name and
+ * file type.
+ */
+struct MatchEntryComparator
+{
+
+ MatchEntryComparator(QString const& name, FileTreeEntry::FileTypes matchTypes)
+ : m_Name(name), m_MatchTypes(matchTypes)
+ {}
+
+ bool operator()(const std::shared_ptr<const FileTreeEntry>& fileEntry) const
+ {
+ return m_MatchTypes.testFlag(fileEntry->fileType()) &&
+ fileEntry->compare(m_Name) == 0;
+ }
+
+ bool operator()(const std::shared_ptr<FileTreeEntry>& fileEntry) const
+ {
+ return m_MatchTypes.testFlag(fileEntry->fileType()) &&
+ fileEntry->compare(m_Name) == 0;
+ }
+
+private:
+ QString const& m_Name;
+ FileTreeEntry::FileTypes m_MatchTypes;
+};
+
+/**
+ *
+ */
+bool IFileTree::exists(QString path, FileTypes type) const
+{
+ return find(path, type) != nullptr;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::find(QString path, FileTypes type)
+{
+ return fetchEntry(splitPath(path), type);
+}
+std::shared_ptr<const FileTreeEntry> IFileTree::find(QString path, FileTypes type) const
+{
+ return fetchEntry(splitPath(path), type);
+}
+
+/**
+ *
+ */
+void IFileTree::walk(
+ std::function<WalkReturn(QString const&, std::shared_ptr<const FileTreeEntry>)>
+ callback,
+ QString sep) const
+{
+
+ std::stack<std::pair<QString, std::shared_ptr<const FileTreeEntry>>> stack;
+
+ // We start by pushing all the entries in this tree, this avoid having to do extra
+ // check later for avoid leading separator:
+ for (auto rit = rbegin(); rit != rend(); ++rit) {
+ stack.push({"", *rit});
+ }
+
+ while (!stack.empty()) {
+ auto [path, entry] = stack.top();
+ stack.pop();
+
+ auto res = callback(path, entry);
+ if (res == WalkReturn::STOP) {
+ break;
+ }
+ if (entry->isDir() && res != WalkReturn::SKIP) {
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ stack.push({path + tree->name() + sep, *rit});
+ }
+ }
+ }
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::addFile(QString path, bool replaceIfExists)
+{
+ QStringList parts = splitPath(path);
+
+ // Check if the file already exists:
+ auto existingEntry = fetchEntry(parts, IFileTree::FILE_OR_DIRECTORY);
+ if (!replaceIfExists && existingEntry != nullptr) {
+ return nullptr;
+ }
+
+ // Find or create the tree:
+ IFileTree* tree;
+
+ if (parts.size() > 1) {
+
+ // Create the tree:
+ std::shared_ptr<FileTreeEntry> treeEntry =
+ createTree(parts.begin(), parts.end() - 1);
+
+ // Early fail if the tree was not created:
+ if (treeEntry == nullptr) {
+ return nullptr;
+ }
+
+ tree = treeEntry->astree().get();
+ } else {
+ tree = this;
+ }
+
+ std::shared_ptr<FileTreeEntry> entry =
+ tree->makeFile(tree->astree(), parts[parts.size() - 1]);
+
+ // If makeFile returns a null pointer, it means we cannot create file:
+ if (entry == nullptr) {
+ return nullptr;
+ }
+
+ // Remove the existing files if there was one:
+ if (existingEntry) {
+ existingEntry->detach();
+ }
+
+ // Insert in the tree:
+ tree->entries().insert(
+ std::upper_bound(tree->begin(), tree->end(), entry, FileEntryComparator{}),
+ entry);
+
+ return entry;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::addDirectory(QString path)
+{
+ QStringList parts = splitPath(path);
+ return createTree(parts.begin(), parts.end());
+}
+
+/**
+ *
+ */
+IFileTree::iterator IFileTree::insert(std::shared_ptr<FileTreeEntry> entry,
+ InsertPolicy insertPolicy)
+{
+
+ // Check that this is not the current tree or a parent tree:
+ if (entry->isDir()) {
+ std::shared_ptr<IFileTree> tmp = astree();
+ while (tmp != nullptr) {
+ if (tmp == entry->astree()) {
+ return end();
+ }
+ tmp = tmp->parent();
+ }
+ }
+
+ // Check if there exists an entry with the same name:
+ auto existingIt = std::find_if(
+ begin(), end(), MatchEntryComparator{entry->name(), FILE_OR_DIRECTORY});
+
+ // Already in the tree?
+ if (existingIt != end() && *existingIt == entry) {
+ return existingIt;
+ }
+
+ auto insertionIt = end();
+
+ if (existingIt != end()) {
+ if (insertPolicy == InsertPolicy::FAIL_IF_EXISTS) {
+ return end();
+ }
+
+ // We replace if the policy is REPLACE or if the new and old entry are
+ // both files:
+ if (insertPolicy == InsertPolicy::REPLACE ||
+ ((*existingIt)->isFile() && entry->isFile())) {
+ if (beforeReplace(this, existingIt->get(), entry.get())) {
+ // Detach the old entry from its parent (not using .detach()
+ // to remove the entry since we are replacing it):
+ (*existingIt)->m_Parent.reset();
+ entries().erase(existingIt);
+
+ // insert at the right place
+ insertionIt = entries().insert(
+ std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry);
+ } else {
+ return end();
+ }
+ } else if ((*existingIt)->isFile() || entry->isFile()) {
+ // If we arrive here and one of the entry is a file, we fail:
+ return end();
+ } else {
+ // If we end up here, we know that the policy is MERGE and that both
+ // are directory that can be merged:
+ mergeTree((*existingIt)->astree(), entry->astree(), nullptr);
+ insertionIt = existingIt;
+ }
+ } else if (beforeInsert(this, entry.get())) {
+ insertionIt = entries().insert(
+ std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry);
+ }
+
+ // Remove the tree from its parent (parent() can be null if we are inserting
+ // a new tree):
+ if (entry->parent() != nullptr) {
+ entry->parent()->erase(entry);
+ }
+
+ // If the entry was actually inserted, we update its parent:
+ if (*insertionIt == entry) {
+ entry->m_Parent = astree();
+ }
+ // Otherwize, we reset it (if this was a merge operation):
+ else {
+ entry->m_Parent.reset();
+ }
+
+ return insertionIt;
+}
+
+/**
+ *
+ */
+bool IFileTree::move(std::shared_ptr<FileTreeEntry> entry, QString path,
+ InsertPolicy insertPolicy)
+{
+
+ // Check that this is not a parent tree:
+ if (entry->isDir()) {
+ std::shared_ptr<IFileTree> tmp = parent();
+ while (tmp != nullptr) {
+ if (tmp == entry->astree()) {
+ return false;
+ }
+ tmp = tmp->parent();
+ }
+ }
+
+ // Insert in folder or replace:
+ const bool insertFolder = path.isEmpty() || path.endsWith("/") || path.endsWith("\\");
+
+ // Retrieve the path:
+ QStringList parts = splitPath(path);
+
+ // Backup the entry name (in case the insertion fails), and update the
+ // name:
+ QString entryName = entry->m_Name;
+ if (!insertFolder) {
+ entry->m_Name = parts.takeLast();
+ }
+
+ // Find or create the tree:
+ IFileTree* tree;
+
+ if (!parts.isEmpty()) {
+
+ // Create the tree:
+ std::shared_ptr<FileTreeEntry> treeEntry = createTree(parts.begin(), parts.end());
+
+ // Early fail if the tree was not created:
+ if (treeEntry == nullptr) {
+ return false;
+ }
+
+ tree = treeEntry->astree().get();
+ } else {
+ tree = this;
+ }
+
+ // We try to insert, and if it fails we need to reset the name:
+ auto it = tree->insert(entry, insertPolicy);
+ if (it == tree->end()) {
+ entry->m_Name = entryName;
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry>
+IFileTree::copy(std::shared_ptr<const FileTreeEntry> entry, QString path,
+ InsertPolicy insertPolicy)
+{
+ // Note: If a conflict exists, the tree is cloned before checking the conflict, so
+ // this is not the most efficient way but copying tree should be pretty rare (and
+ // probably avoided anyway), and this allow us to use `move()` to do all the complex
+ // operations.
+ auto clone = entry->clone();
+ if (move(clone, path, insertPolicy)) {
+ return clone;
+ }
+ return nullptr;
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::merge(std::shared_ptr<IFileTree> source,
+ OverwritesType* overwrites)
+{
+
+ // Check that this is not a parent tree:
+ std::shared_ptr<IFileTree> tmp = astree();
+ while (tmp != nullptr) {
+ if (tmp == source) {
+ return MERGE_FAILED;
+ }
+ tmp = tmp->parent();
+ }
+
+ return mergeTree(astree(), source, overwrites);
+}
+
+/**
+ *
+ */
+IFileTree::iterator IFileTree::erase(std::shared_ptr<FileTreeEntry> entry)
+{
+
+ if (!beforeRemove(this, entry.get())) {
+ return end();
+ }
+
+ auto it = std::find(begin(), end(), entry);
+ if (it == end()) {
+ return it;
+ }
+ entry->m_Parent.reset();
+ return entries().erase(it);
+ ;
+}
+
+/**
+ *
+ */
+std::pair<IFileTree::iterator, std::shared_ptr<FileTreeEntry>>
+IFileTree::erase(QString name)
+{
+ auto it = std::find_if(begin(), end(), [&name](const auto& entry) {
+ return entry->compare(name) == 0;
+ });
+
+ if (it == end()) {
+ return {it, nullptr};
+ }
+
+ if (!beforeRemove(this, it->get())) {
+ return {end(), nullptr};
+ }
+
+ // Save the entry to return it:
+ auto entry = *it;
+ entry->m_Parent.reset();
+
+ return {entries().erase(it), entry};
+}
+
+/**
+ *
+ */
+bool IFileTree::clear()
+{
+ // Need to find the iterator up to which we should erase:
+ auto& entries_ = entries();
+ auto it = entries_.begin();
+ for (; it != entries_.end() && beforeRemove(this, it->get()); ++it) {
+ // Detach (but not remove from the vector):
+ (*it)->m_Parent.reset();
+ }
+ entries_.erase(entries_.begin(), it);
+ return empty();
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::removeAll(QStringList names)
+{
+ return removeIf([&names](auto& entry) {
+ return names.contains(entry->name(), Qt::CaseInsensitive);
+ });
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::removeIf(
+ std::function<bool(std::shared_ptr<FileTreeEntry> const&)> predicate)
+{
+ std::size_t osize = size();
+ auto& en = entries();
+ // Cannot use begin() and end() directly because those are immutable iterators:
+ en.erase(std::remove_if(en.begin(), en.end(),
+ [this, &predicate](auto& entry) {
+ return beforeRemove(this, entry.get()) && predicate(entry);
+ }),
+ en.end());
+ return osize - size();
+}
+
+/**
+ *
+ */
+QStringList IFileTree::splitPath(QString path)
+{
+ // Using raw \\ instead of QDir::separator() since we are replacing by /
+ // anyway, and this avoid pulling an extra header (like QDir) only
+ // for the separator.
+ return path.replace("\\", "/").split("/", Qt::SkipEmptyParts);
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeReplace(IFileTree const*, FileTreeEntry const*,
+ FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeInsert(IFileTree const*, FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeRemove(IFileTree const*, FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::mergeTree(std::shared_ptr<IFileTree> destination,
+ std::shared_ptr<IFileTree> source,
+ OverwritesType* overwrites)
+{
+
+ const auto comp = FileEntryComparator{};
+
+ // Number of overwritten entries:
+ std::size_t noverwrites = 0;
+
+ // Note: Using the iterators from the vector directly since the other ones
+ // cannot be assigned to.
+ auto &dstEntries = destination->entries(), &srcEntries = source->entries();
+
+ // Note: Since entries are not sorted by name but by type (file/directory)
+ // then name, there is no fast way to check for conflict.
+ for (auto& srcEntry : srcEntries) {
+
+ // Try to find an exact match (name and type) - This iterator also
+ // serve to know where the entry should be inserted:
+ auto dstIt = std::lower_bound(dstEntries.begin(), dstEntries.end(), srcEntry, comp);
+
+ // Exact match found:
+ if (dstIt != dstEntries.end() && (*dstIt)->compare(srcEntry->name()) == 0 &&
+ (*dstIt)->isFile() == srcEntry->isFile()) {
+
+ // Both directory, we merge:
+ if ((*dstIt)->isDir() && srcEntry->isDir()) {
+ noverwrites += mergeTree((*dstIt)->astree(), srcEntry->astree(), overwrites);
+
+ // Detach the entry:
+ srcEntry->m_Parent.reset();
+ }
+ // Otherwize, check if the source can replace the destination:
+ else if (beforeReplace(destination.get(), dstIt->get(), srcEntry.get())) {
+ // Remove the parent:
+ auto dstEntry = *dstIt;
+ dstEntry->m_Parent.reset();
+
+ // Update overwrites information:
+ noverwrites++;
+ if (overwrites != nullptr) {
+ overwrites->insert({dstEntry, srcEntry});
+ }
+
+ // Replace the destination:
+ *dstIt = srcEntry;
+ srcEntry->m_Parent = destination;
+ }
+ // If not, fails:
+ else {
+ return MERGE_FAILED;
+ }
+ } else {
+ // If we did not find a match, the only way to check is to look
+ // through the vector:
+ auto conflictIt = std::find_if(dstEntries.begin(), dstEntries.end(),
+ [name = srcEntry->name()](auto const& dstEntry) {
+ return dstEntry->compare(name) == 0;
+ });
+
+ // Conflict (note that here both entries are of different types, so no need to
+ // check if we replace or merge):
+ int deleteIndex = -1;
+ if (conflictIt != dstEntries.end()) {
+
+ // We check if we can replace the entry:
+ if (!beforeReplace(destination.get(), conflictIt->get(), srcEntry.get())) {
+ return MERGE_FAILED;
+ }
+
+ // We need to store the index because the insert() will mess up the
+ // iterators:
+ deleteIndex = static_cast<int>(conflictIt - std::begin(dstEntries));
+ if (dstIt < conflictIt) {
+ deleteIndex += 1;
+ }
+
+ // Detach the conflicting entry (we erase it later, after the insertion):
+ (*conflictIt)->m_Parent.reset();
+
+ // Update overwrites information:
+ noverwrites++;
+ if (overwrites != nullptr) {
+ overwrites->insert({*conflictIt, srcEntry});
+ }
+ }
+ // No conflict, we still have to check if we can insert:
+ else if (!beforeInsert(destination.get(), srcEntry.get())) {
+ return MERGE_FAILED;
+ }
+
+ // Insert the entry using the previous iterator:
+ dstEntries.insert(dstIt, srcEntry);
+
+ // We delete here:
+ if (deleteIndex != -1) {
+ dstEntries.erase(dstEntries.begin() + deleteIndex);
+ }
+
+ // Update the parent:
+ srcEntry->m_Parent = destination;
+ }
+ }
+
+ // Clear the sources:
+ srcEntries.clear();
+
+ return noverwrites;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::createOrphanTree(QString name) const
+{
+ auto directory = makeDirectory(nullptr, name);
+ if (directory != nullptr) {
+ directory->m_Populated = true;
+ }
+ return directory;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::fetchEntry(QStringList path,
+ FileTypes matchTypes)
+{
+ return std::const_pointer_cast<FileTreeEntry>(
+ const_cast<const IFileTree*>(this)->fetchEntry(path, matchTypes));
+}
+std::shared_ptr<const FileTreeEntry> IFileTree::fetchEntry(QStringList const& path,
+ FileTypes matchTypes) const
+{
+ // Check to ensure that the path contains at least one element:
+ if (path.isEmpty()) {
+ return nullptr;
+ }
+
+ // Early check:
+ if (path[path.size() - 1].startsWith("*")) {
+ return nullptr;
+ }
+
+ const IFileTree* tree = this;
+ auto it = std::begin(path);
+ for (; tree != nullptr && it != std::end(path) - 1; ++it) {
+ // Special cases:
+ if (*it == ".") {
+ continue;
+ } else if (*it == "..") {
+ tree = tree->parent().get();
+ } else {
+ // Find the entry at the current level:
+ auto entryIt = std::find_if(tree->begin(), tree->end(),
+ MatchEntryComparator{*it, IFileTree::DIRECTORY});
+
+ // Early exists if the entry does not exist or is not a directory:
+ if (entryIt == tree->end()) {
+ tree = nullptr;
+ } else {
+ tree = (*entryIt)->astree().get();
+ }
+ }
+ }
+
+ if (tree == nullptr) {
+ return nullptr;
+ }
+
+ // We have the final tree:
+ auto entryIt =
+ std::find_if(tree->begin(), tree->end(), MatchEntryComparator{*it, matchTypes});
+ auto bIt = tree->end();
+ return entryIt == bIt ? nullptr : *entryIt;
+}
+
+/**
+ * @brief Create a new file under this tree.
+ *
+ * @param parent The current tree, without const-qualification.
+ * @param name Name of the file.
+ * @param time Modification time of the file.
+ *
+ * @return the created file.
+ */
+std::shared_ptr<FileTreeEntry>
+IFileTree::makeFile(std::shared_ptr<const IFileTree> parent, QString name) const
+{
+ return createFileEntry(parent, name);
+}
+
+/**
+ *
+ */
+IFileTree::IFileTree() {}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::clone() const
+{
+ std::shared_ptr<IFileTree> tree = doClone();
+
+ // Don't copy not populated tree, it is not useful:
+ if (m_Populated) {
+ tree->m_Populated = true;
+ auto& tentries = tree->m_Entries;
+ for (auto e : entries()) {
+ auto ce = e->clone();
+ ce->m_Parent = tree;
+ tentries.push_back(ce);
+ }
+ }
+
+ return tree;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::createTree(QStringList::const_iterator begin,
+ QStringList::const_iterator end)
+{
+ // The current tree and entry:
+ std::shared_ptr<IFileTree> tree = astree();
+ for (auto it = begin; tree != nullptr && it != end; ++it) {
+ // Special cases:
+ if (*it == ".") {
+ continue;
+ } else if (*it == "..") {
+ // parent() returns nullptr if it does not exist, so no
+ // check required:
+ tree = parent();
+ } else {
+
+ // Check if the entry exists (looking for both files and directories
+ // because we don't want to override a file):
+ auto entryIt =
+ std::find_if(tree->begin(), tree->end(),
+ MatchEntryComparator{*it, IFileTree::FILE_OR_DIRECTORY});
+
+ // Create if it does not:
+ if (entryIt == tree->end()) {
+ auto newTree = tree->makeDirectory(tree, *it);
+
+ // If makeDirectory returns a null pointer, it means we cannot create tree.
+ if (newTree == nullptr) {
+ tree = nullptr;
+ break;
+ }
+
+ // The tree is empty so already populated:
+ newTree->m_Populated = true;
+
+ tree->entries().insert(std::upper_bound(tree->begin(), tree->end(), newTree,
+ FileEntryComparator{}),
+ newTree);
+ tree = newTree;
+ } else if ((*entryIt)->isDir()) {
+ tree = (*entryIt)->astree();
+ } else { // Cannot go further:
+ tree = nullptr;
+ }
+ }
+ }
+
+ return tree;
+}
+
+/**
+ * @brief Retrieve the vector of entries after populating it if required.
+ *
+ * @return the vector of entries.
+ */
+std::vector<std::shared_ptr<FileTreeEntry>>& IFileTree::entries()
+{
+ std::call_once(m_OnceFlag, [this]() {
+ populate();
+ });
+ return m_Entries;
+}
+const std::vector<std::shared_ptr<FileTreeEntry>>& IFileTree::entries() const
+{
+ std::call_once(m_OnceFlag, [this]() {
+ populate();
+ });
+ return m_Entries;
+}
+
+/**
+ * @brief Populate the internal vectors and update the flag.
+ */
+void IFileTree::populate() const
+{
+ // Need to check m_Populated again here since the tree can be populated without
+ // a call to entries() (e.g., on copy/orphanTree):
+ if (!m_Populated) {
+ if (!doPopulate(astree(), m_Entries)) {
+ std::sort(std::begin(m_Entries), std::end(m_Entries), FileEntryComparator{});
+ }
+ m_Populated = true;
+ }
+}
+
+// walk and glob with generator
+
+std::generator<std::shared_ptr<const FileTreeEntry>>
+walk(std::shared_ptr<const IFileTree> fileTree)
+{
+ std::stack<std::shared_ptr<const FileTreeEntry>> stack;
+
+ // we start by pushing all the entries in this tree, this avoid having to do extra
+ // check later to avoid leading separator
+ for (auto rit = fileTree->rbegin(); rit != fileTree->rend(); ++rit) {
+ stack.push(*rit);
+ }
+
+ while (!stack.empty()) {
+ auto entry = stack.top();
+ stack.pop();
+
+ co_yield entry;
+
+ if (entry->isDir()) {
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ stack.push(*rit);
+ }
+ }
+ }
+}
+
+namespace
+{
+ using glob_stack_t = std::stack<
+ std::pair<std::shared_ptr<const FileTreeEntry>, std::span<QRegularExpression>>>;
+
+ // check the given entry against the list of patterns and add new (entry, patterns) to
+ // the given stack
+ //
+ std::generator<std::shared_ptr<const FileTreeEntry>>
+ ifiletree_glob_impl_step(glob_stack_t& glob_stack,
+ std::shared_ptr<const FileTreeEntry> entry,
+ std::span<QRegularExpression> const& patterns)
+ {
+ // no more patterns, nothing to do
+ if (patterns.size() == 0) {
+ co_return;
+ }
+
+ // special handling if the pattern is empty (correspond to a '**' glob)
+ if (patterns[0].pattern() == "") {
+
+ // if there are more patterns after '**', we need to check the entry again, e.g.,
+ // if the entry name is 'x' and the pattern is '**/x' to match it
+ if (patterns.size() != 1) {
+ glob_stack.emplace(entry, patterns.subspan(1));
+ }
+
+ // if the entry is a file, there is nothing to do with '**'
+ if (entry->isFile()) {
+ co_return;
+ }
+
+ // if this is the end of the patterns list, we need to yield the current entry
+ // since it is a directory
+ if (patterns.size() == 1) {
+ co_yield entry;
+ }
+
+ // recurse over childs, but for directories, we need to keep the leading '**' in
+ // the list of patterns since '**' can match multiple level of directories
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ glob_stack.emplace(*rit, (*rit)->isDir() ? patterns : patterns.subspan(1));
+ }
+ }
+ // otherwise (if the first patterns is not '**'), we simply check if we have a match
+ else if (patterns[0].match(entry->name()).hasMatch()) {
+ // this was the last pattern and we have a match, so we yield the current entry,
+ // not that this will yield intermediate matching directory, but this is expected
+ if (patterns.size() == 1) {
+ co_yield entry;
+ }
+
+ // if the entry is not a directory, we have nothing more to do
+ if (entry->isFile()) {
+ co_return;
+ }
+
+ // if all that remain after this pattern is a '**', we need to yield the current
+ // entry since '**' can also match an empty succession of directories, e.g. 'a/b'
+ // is matched by 'a/b/**'
+ if (patterns.size() == 2 && patterns[1].pattern() == "") {
+ co_yield entry;
+ }
+
+ // we then need to recurse over
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ glob_stack.emplace(*rit, patterns.subspan(1));
+ }
+ }
+ }
+
+ // main function for IFileTree::glob - this function simply manages the stack of of
+ // entry / patterns to handle, and then falls back to ifiletree_glob_impl_step
+ //
+ std::generator<std::shared_ptr<const FileTreeEntry>>
+ ifiletree_glob_impl(std::shared_ptr<const FileTreeEntry> entry,
+ std::span<QRegularExpression> const& patterns)
+ {
+ glob_stack_t stack;
+ stack.emplace(entry, patterns);
+
+ while (!stack.empty()) {
+ const auto [childEntry, childPatterns] = stack.top();
+ stack.pop();
+ co_yield std::ranges::elements_of(
+ ifiletree_glob_impl_step(stack, childEntry, childPatterns));
+ }
+ }
+
+} // namespace
+
+/**
+ *
+ */
+std::generator<std::shared_ptr<const FileTreeEntry>>
+glob(std::shared_ptr<const IFileTree> fileTree, QString pattern,
+ GlobPatternType patternType)
+{
+ // replace \\ by / to simply handling
+ pattern = pattern.trimmed().replace("\\", "/");
+
+ // reduce successions of **/** to **, this makes it easier to handle it in the
+ // actual implementation
+ pattern = pattern.replace(QRegularExpression("(\\*\\*/)*\\*\\*"), "**");
+
+ // we are going to match directly starting from the child of the tree, so we need
+ // to handle the root here
+ //
+ // note: this is the only pattern that can match the tree itself
+ if (pattern == "**") {
+ co_yield fileTree;
+ }
+
+ // split pattern into blocks, we keep
+ const auto regexOptions = FileNameComparator::CaseSensitivity == Qt::CaseInsensitive
+ ? QRegularExpression::CaseInsensitiveOption
+ : QRegularExpression::NoPatternOption;
+ std::vector<QRegularExpression> patterns;
+ for (const auto& part : pattern.split("/")) {
+ if (part == "**") {
+ // for '**' pattern, push an empty regex
+ patterns.emplace_back();
+ } else {
+ const auto regexpPattern =
+ patternType == GlobPatternType::GLOB
+ ? QRegularExpression::wildcardToRegularExpression(
+ part, QRegularExpression::NonPathWildcardConversion)
+ : part;
+ const QRegularExpression regex(regexpPattern, regexOptions);
+
+ if (!regex.isValid()) {
+ throw InvalidGlobPatternException(regex.errorString());
+ }
+
+ patterns.push_back(regex);
+ }
+ }
+
+ // fall back to the main glob function for the child of this tree
+ for (const auto& child : *fileTree) {
+ co_yield std::ranges::elements_of(ifiletree_glob_impl(child, patterns));
+ }
+}
+
+} // namespace MOBase