diff --git a/src/wildcard.cpp b/src/wildcard.cpp index f5404650a..7da596642 100644 --- a/src/wildcard.cpp +++ b/src/wildcard.cpp @@ -722,6 +722,7 @@ void wildcard_expander_t::expand_intermediate_segment(const wcstring &base_dir, const wcstring &prefix) { std::string narrow; const dir_iter_t::entry_t *entry{}; + bool is_final = !*wc_remainder && wc_segment.find(ANY_STRING_RECURSIVE) == wcstring::npos; while (!interrupted_or_overflowed() && (entry = base_dir_iter.next())) { // Note that it's critical we ignore leading dots here, else we may descend into . and .. if (!wildcard_match(entry->name, wc_segment, true)) { @@ -733,6 +734,22 @@ void wildcard_expander_t::expand_intermediate_segment(const wcstring &base_dir, continue; } + // Fast path: If this entry can't be a link (we know via d_type), + // we don't need to protect against symlink loops. + // This is *not* deduplication, we just don't want a loop. + // + // We only do this when we are the last `*/` component, + // because we're a bit inconsistent on when we will enter loops. + if (is_final && !entry->is_possible_link()) { + // We made it through. + // Perform normal wildcard expansion on this new directory, + // starting at our tail_wc + wcstring full_path = base_dir + entry->name; + full_path.push_back(L'/'); + this->expand(full_path, wc_remainder, prefix + wc_segment + L'/'); + continue; + } + auto statbuf = entry->stat(); if (!statbuf) { continue; @@ -744,8 +761,7 @@ void wildcard_expander_t::expand_intermediate_segment(const wcstring &base_dir, continue; } - // We made it through. Perform normal wildcard expansion on this new directory, starting at - // our tail_wc, which includes the ANY_STRING_RECURSIVE guy. + // (like the fast path above) wcstring full_path = base_dir + entry->name; full_path.push_back(L'/'); this->expand(full_path, wc_remainder, prefix + wc_segment + L'/'); diff --git a/src/wutil.cpp b/src/wutil.cpp index 5914c1ca3..ef987f952 100644 --- a/src/wutil.cpp +++ b/src/wutil.cpp @@ -228,10 +228,12 @@ const dir_iter_t::entry_t *dir_iter_t::next() { entry_.inode = dent->d_ino; #ifdef HAVE_STRUCT_DIRENT_D_TYPE auto type = dirent_type_to_entry_type(dent->d_type); - // Do not store symlinks as we will need to resolve them. + // Do not store symlinks as type as we will need to resolve them. if (type != dir_entry_type_t::lnk) { entry_.type_ = type; } + // This entry could be a link if it is a link or unknown. + entry_.possible_link_ = !type.has_value() || type == dir_entry_type_t::lnk; #endif return &entry_; } diff --git a/src/wutil.h b/src/wutil.h index a0565faee..5ab2d4bdc 100644 --- a/src/wutil.h +++ b/src/wutil.h @@ -221,6 +221,9 @@ class dir_iter_t : noncopyable_t { /// \return whether this is a directory. This may call stat(). bool is_dir() const { return check_type() == dir_entry_type_t::dir; } + /// \return false if we know this can't be a link via d_type, true if it could be. + bool is_possible_link() const { return possible_link_; } + /// \return the stat buff for this entry, invoking stat() if necessary. const maybe_t &stat() const; @@ -239,6 +242,9 @@ class dir_iter_t : noncopyable_t { // and the type is left as none(). Note this is an unavoidable race. mutable maybe_t type_{}; + /// whether this entry could be a link, false if we know definitively it isn't. + bool possible_link_ = true; + // fd of the DIR*, used for fstatat(). int dirfd_{-1};