current commit
261019.11 · fs: reduce glob section allocations
#include "glob/glob_matcher.h"

#include <algorithm>
#include <bit>
#include <limits>
#include <ranges>
#include <span>

#include "glob/glob_parser.h"
#include "glob/glob_unicode.h"
#include "util-inl.h"

namespace node::glob {

namespace {

// The set of positions in the subject a segment's program can currently be
// at. Carrying every position at once is what makes matching linear in
// (positions x nodes) instead of backtracking, so a pattern like
// `*(a|aa)*(a|aa)b` costs the same as any other.
class PosSet {
 public:
  void Init(size_t positions) {
    nwords_ = (positions + kBitsPerWord - 1) / kBitsPerWord;
    if (nwords_ > kInlineWords)
      heap_.assign(nwords_, 0);
    else
      std::fill_n(inline_, nwords_, 0);
  }
  void Set(size_t i) {
    words()[i >> kWordShift] |= uint64_t{1} << (i & kBitInWordMask);
  }
  bool Test(size_t i) const {
    return (words()[i >> kWordShift] >> (i & kBitInWordMask)) & 1;
  }
  bool Empty() const {
    return std::ranges::all_of(words(), [](uint64_t v) { return v == 0; });
  }
  template <typename F>
  void ForEach(F&& f) const {
    const std::span<const uint64_t> ws = words();
    for (size_t w = 0; w < nwords_; w++) {
      uint64_t word = ws[w];
      while (word != 0) {
        f(w * kBitsPerWord + std::countr_zero(word));
        word &= word - 1;
      }
    }
  }
  void UnionWith(const PosSet& other) {
    const std::span<uint64_t> w = words();
    const std::span<const uint64_t> o = other.words();
    for (size_t i = 0; i < nwords_; i++) w[i] |= o[i];
  }

 private:
  static constexpr size_t kBitsPerWord = std::numeric_limits<uint64_t>::digits;
  static constexpr size_t kWordShift = std::countr_zero(kBitsPerWord);
  static constexpr size_t kBitInWordMask = kBitsPerWord - 1;
  // Four words hold 256 positions, one more than the longest name the
  // common filesystems allow, so only a synthetic subject reaches the heap.
  static constexpr size_t kInlineWords = 4;

  std::span<uint64_t> words() {
    return {nwords_ <= kInlineWords ? inline_ : heap_.data(), nwords_};
  }
  std::span<const uint64_t> words() const {
    return {nwords_ <= kInlineWords ? inline_ : heap_.data(), nwords_};
  }

  size_t nwords_ = 0;
  uint64_t inline_[kInlineWords];
  std::vector<uint64_t> heap_;
};

template <typename Char>
class SegmentEvaluator {
 public:
  using View = std::basic_string_view<Char>;

  SegmentEvaluator(const SegmentProgram& p, View in)
      : p_(p), in_(in), n_(in.size()) {}

  bool Accepts() {
    if (p_.start_no_traversal && IsDotSegment()) return false;
    if (p_.start_no_dot && in_.starts_with('.')) return false;
    PosSet start;
    start.Init(n_ + 1);
    start.Set(0);
    PosSet out = Reach(p_.root, start);
    return out.Test(n_);
  }

 private:
  bool IsDotSegment() const {
    return (n_ == 1 && in_[0] == '.') ||
           (n_ == 2 && in_[0] == '.' && in_[1] == '.');
  }

  // one code point (uflag) or code unit (non-u) at pos; len ∈ {1, 2}.
  uint32_t CharAt(size_t pos, size_t* len) const {
    const uint32_t c = in_[pos];
    if constexpr (sizeof(Char) > 1) {
      if (p_.uflag && IsUnicodeLead(c) && pos + 1 < n_ &&
          IsUnicodeTrail(in_[pos + 1])) {
        *len = 2;
        return CombineSurrogates(c, in_[pos + 1]);
      }
    }
    *len = 1;
    return c;
  }

  bool UnitsEqualFolded(uint32_t a, uint32_t b) const {
    if (a == b) return true;
    if (!p_.nocase) return false;
    return Canonicalize(a, p_.uflag) == Canonicalize(b, p_.uflag);
  }

  // Match a literal run at pos; returns end position or SIZE_MAX.
  size_t TextAt(const PatternString& text, size_t pos) const {
    if (!p_.nocase) {
      if (pos + text.size() > n_) return SIZE_MAX;
      for (size_t i = 0; i < text.size(); i++) {
        if (static_cast<uint32_t>(in_[pos + i]) !=
            static_cast<uint32_t>(text[i])) {
          return SIZE_MAX;
        }
      }
      return pos + text.size();
    }
    // folded comparison, unit-wise (non-u) or cp-wise (u)
    size_t ip = pos;
    size_t tp = 0;
    while (tp < text.size()) {
      if (ip >= n_) return SIZE_MAX;
      if (p_.uflag && sizeof(Char) > 1) {
        size_t il;
        const uint32_t ic = CharAt(ip, &il);
        const char16_t tc0 = text[tp];
        uint32_t tc = tc0;
        size_t tl = 1;
        if (IsUnicodeLead(tc0) && tp + 1 < text.size() &&
            IsUnicodeTrail(text[tp + 1])) {
          tc = CombineSurrogates(tc0, text[tp + 1]);
          tl = 2;
        }
        if (!UnitsEqualFolded(ic, tc)) return SIZE_MAX;
        ip += il;
        tp += tl;
      } else {
        if (!UnitsEqualFolded(in_[ip], text[tp])) return SIZE_MAX;
        ip++;
        tp++;
      }
    }
    return ip;
  }

  // A class matches when some member is case-equivalent to the subject, and
  // only then is the polarity applied.
  bool ClassMatches(const RxNode::CharClass& k, uint32_t c) const {
    if (p_.nocase && !p_.uflag) c = Canonicalize(c, /*fold=*/false);
    if (k.has_positive && (k.positive.Contains(c) != k.negate)) return true;
    if (k.has_negative && (k.negative.Contains(c) == k.negate)) return true;
    return false;
  }

  // [^/]*? expansion from position p into `out`
  void StarChain(size_t p, bool no_empty, PosSet* out) const {
    size_t q = p;
    bool first = true;
    while (true) {
      if (!first || !no_empty) {
        if (out->Test(q)) return;  // chain from q already marked
        out->Set(q);
      }
      if (q >= n_ || in_[q] == '/') return;
      size_t len;
      CharAt(q, &len);
      q += len;
      first = false;
    }
  }

  bool GuardMatchesToEnd(const RxNode& neg, size_t p) {
    PosSet start;
    start.Init(n_ + 1);
    start.Set(p);
    for (uint32_t child : neg.children) {
      PosSet r = Reach(child, start);
      if (r.Test(n_)) return true;
    }
    return false;
  }

  // Kleene closure of `body` applied to `seed`.
  PosSet Closure(uint32_t body, const PosSet& seed) {
    PosSet out = seed;
    PosSet frontier = seed;
    while (!frontier.Empty()) {
      PosSet next = Reach(body, frontier);
      PosSet fresh;
      fresh.Init(n_ + 1);
      bool any = false;
      next.ForEach([&](size_t pos) {
        if (!out.Test(pos)) {
          fresh.Set(pos);
          out.Set(pos);
          any = true;
        }
      });
      if (!any) break;
      frontier = fresh;
    }
    return out;
  }

  PosSet Reach(uint32_t id, const PosSet& in) {
    const RxNode& node = p_.nodes[id];
    PosSet out;
    out.Init(n_ + 1);
    switch (node.kind) {
      case RxNode::Kind::kEmpty:
        return in;
      case RxNode::Kind::kAssert:
        in.ForEach([&](size_t pos) {
          if (node.assert_no_dot && pos < n_ && in_[pos] == '.') return;
          if (node.assert_no_traversal && pos == 0 && IsDotSegment()) {
            return;
          }
          out.Set(pos);
        });
        return out;
      case RxNode::Kind::kText:
        in.ForEach([&](size_t pos) {
          const size_t end = TextAt(node.text, pos);
          if (end != SIZE_MAX) out.Set(end);
        });
        return out;
      case RxNode::Kind::kAnyChar:
        in.ForEach([&](size_t pos) {
          if (pos >= n_ || in_[pos] == '/') return;
          size_t len;
          CharAt(pos, &len);
          out.Set(pos + len);
        });
        return out;
      case RxNode::Kind::kClass:
        in.ForEach([&](size_t pos) {
          if (pos >= n_) return;
          size_t len;
          const uint32_t c = CharAt(pos, &len);
          if (ClassMatches(node.klass, c)) out.Set(pos + len);
        });
        return out;
      case RxNode::Kind::kStarAny:
        in.ForEach([&](size_t pos) {
          if (node.star_no_dot && pos < n_ && in_[pos] == '.') return;
          StarChain(pos, node.star_no_empty, &out);
        });
        return out;
      case RxNode::Kind::kConcat: {
        PosSet cur = in;
        for (uint32_t child : node.children) {
          cur = Reach(child, cur);
          if (cur.Empty()) break;
        }
        return cur;
      }
      case RxNode::Kind::kAlt: {
        for (uint32_t child : node.children) {
          PosSet r = Reach(child, in);
          out.UnionWith(r);
        }
        return out;
      }
      case RxNode::Kind::kOpt: {
        out = in;
        PosSet r = Reach(node.children[0], in);
        out.UnionWith(r);
        return out;
      }
      case RxNode::Kind::kStar:
        return Closure(node.children[0], in);
      case RxNode::Kind::kPlus: {
        PosSet once = Reach(node.children[0], in);
        return Closure(node.children[0], once);
      }
      case RxNode::Kind::kTwoBody: {
        // (?:first)(?:rest)*?
        PosSet first = Reach(node.children[0], in);
        return Closure(node.children[1], first);
      }
      case RxNode::Kind::kNeg: {
        in.ForEach([&](size_t pos) {
          if (node.star_no_dot && pos < n_ && in_[pos] == '.') return;
          if (GuardMatchesToEnd(node, pos)) return;
          StarChain(pos, /*no_empty=*/false, &out);
        });
        return out;
      }
    }
    return out;
  }

  const SegmentProgram& p_;
  View in_;
  const size_t n_;
};

template <typename Char>
bool EndsWith(std::basic_string_view<Char> s, PatternView suffix) {
  return s.size() >= suffix.size() &&
         UnitsEqual(s.substr(s.size() - suffix.size()), suffix);
}

// `f.toLowerCase().endsWith(ext.toLowerCase())`
template <typename Char>
bool EndsWithLowered(std::basic_string_view<Char> f, PatternView lowered_ext) {
  const bool ascii = std::ranges::all_of(
      f, [](Char c) { return static_cast<uint32_t>(c) <= kMaxAsciiCodePoint; });
  if (ascii) {
    return f.size() >= lowered_ext.size() &&
           std::ranges::equal(lowered_ext,
                              f.substr(f.size() - lowered_ext.size()),
                              [](char16_t ext, Char c) {
                                return static_cast<uint32_t>(ext) ==
                                       AsciiLower(static_cast<uint32_t>(c));
                              });
  }
  PatternString widened(f.begin(), f.end());
  PatternString lowered;
  AppendLowered(widened, &lowered);
  return EndsWith<char16_t>(lowered, lowered_ext);
}

template <typename Char>
bool IsDots(std::basic_string_view<Char> f) {
  return (f.size() == 1 && f[0] == '.') ||
         (f.size() == 2 && f[0] == '.' && f[1] == '.');
}

template <typename Char>
bool Equals(std::basic_string_view<Char> f, PatternView text) {
  return UnitsEqual(f, text);
}

// /^text$/i without the 'u' flag: code units compared under Canonicalize
template <typename Char>
bool EqualsFolded(std::basic_string_view<Char> f, PatternView text) {
  if (f.size() != text.size()) return false;
  for (size_t i = 0; i < f.size(); i++) {
    const uint32_t a = static_cast<uint32_t>(f[i]);
    const uint32_t b = static_cast<uint32_t>(text[i]);
    if (a != b && Canonicalize(a, false) != Canonicalize(b, false)) {
      return false;
    }
  }
  return true;
}

template <typename Char>
bool TestSegmentImpl(const SegmentProgram& p, std::basic_string_view<Char> f) {
  switch (p.tier) {
    case SegmentProgram::Tier::kEmpty:
      return f.empty();
    case SegmentProgram::Tier::kLiteral:
      if (p.nocase) return EqualsFolded(f, p.literal);
      return Equals(f, p.literal);
    case SegmentProgram::Tier::kStar:
      // starTest / starTestDot
      if (f.empty()) return false;
      return p.dot ? !IsDots(f) : !f.starts_with('.');
    case SegmentProgram::Tier::kStarDotExt: {
      if (!p.dot && f.starts_with('.')) return false;
      if (p.nocase) return EndsWithLowered(f, p.literal_lower);
      return EndsWith(f, p.literal);
    }
    case SegmentProgram::Tier::kStarDotStar: {
      const bool has_dot =
          f.find(static_cast<Char>('.')) != std::basic_string_view<Char>::npos;
      if (p.dot) return !IsDots(f) && has_dot;
      return !f.starts_with('.') && has_dot;
    }
    case SegmentProgram::Tier::kDotStar:
      return !IsDots(f) && f.starts_with('.');
    case SegmentProgram::Tier::kQmarks: {
      if (f.size() != p.qmarks_len) return false;
      if (p.dot) {
        if (IsDots(f)) return false;
      } else if (f.starts_with('.')) {
        return false;
      }
      if (p.literal.empty()) return true;
      if (p.nocase) return EndsWithLowered(f, p.literal_lower);
      return EndsWith(f, p.literal);
    }
    case SegmentProgram::Tier::kRegex: {
      SegmentEvaluator<Char> ev(p, f);
      return ev.Accepts();
    }
  }
  return false;
}

template <typename Char>
bool TestPartImpl(const PartMatcher& part, std::basic_string_view<Char> f) {
  switch (part.kind) {
    case PartMatcher::Kind::kGlobstar:
      return false;  // handled structurally by the path matcher
    case PartMatcher::Kind::kLiteral:
      return Equals(f, part.literal);
    case PartMatcher::Kind::kProgram:
      return TestSegmentImpl(part.program, f);
  }
  return false;
}

}  // namespace

bool TestPart(const PartMatcher& part, PatternView f) {
  return TestPartImpl<char16_t>(part, f);
}

bool TestPart(const PartMatcher& part, OneByteView f) {
  return TestPartImpl<char8_t>(part, f);
}

namespace {

template <typename Char>
class PathMatcher {
 public:
  using View = std::basic_string_view<Char>;
  // The file's path parts as views
  using Parts = std::span<const View>;

  explicit PathMatcher(const CompiledPattern& pat) : pat_(&pat) {}

  bool Match(View path) {
    if (pat_->empty) return path.empty();
    View f = path;
    if (pat_->flags.windows &&
        path.find(static_cast<Char>('\\')) != View::npos) {
      rewritten_.assign(path.begin(), path.end());
      for (Char& c : rewritten_) {
        if (c == '\\') c = '/';
      }
      f = View(rewritten_.data(), rewritten_.size());
    }
    Parts file = SplitViews(f);

    View basename;
    if (pat_->flags.match_base) {
      for (const View& part : file | std::views::reverse) {
        if (!part.empty()) {
          basename = part;
          break;
        }
      }
    }
    if (std::ranges::any_of(file, IsDots<Char>)) file = NormalizeFile(file);
    for (const CompiledPattern::Row& row : pat_->rows) {
      if (pat_->flags.match_base && row.parts.size() == 1) {
        const Parts only{&basename, 1};
        if (MatchOne(only, row)) return true;
        continue;
      }
      if (MatchOne(file, row)) return true;
    }
    return false;
  }

 private:
  using RowParts = std::vector<PartMatcher>;
  // Path depth held without allocating. Deeper paths still work; they just
  // grow the buffer, and they are rare enough not to matter.
  static constexpr size_t kInlineParts = 32;

  // Minimatch#slashSplit
  Parts SplitViews(View f) {
    size_t count = 0;
    auto push = [&](View v) {
      if (count == parts_.capacity()) {
        parts_.SetLength(count);
        parts_.AllocateSufficientStorage(count * 2);
      }
      parts_.out()[count++] = v;
    };
    SlashSplitInto(f, pat_->flags.windows, push);
    return {parts_.out(), count};
  }

  Parts NormalizeFile(Parts file) {
    owned_.clear();
    owned_.reserve(file.size());
    for (size_t i = 0; i < file.size(); i++) {
      owned_.emplace_back(file[i].begin(), file[i].end());
    }
    LevelTwoFileOptimize(&owned_, pat_->flags);
    owned_views_.clear();
    owned_views_.reserve(owned_.size());
    for (const std::basic_string<Char>& s : owned_) {
      owned_views_.emplace_back(s);
    }
    return {owned_views_.data(), owned_views_.size()};
  }

  static bool PartIsLiteral(const PartMatcher& p, PatternView text) {
    return p.kind == PartMatcher::Kind::kLiteral &&
           PatternView(p.literal) == text;
  }

  bool MatchOne(Parts file, const CompiledPattern::Row& row) {
    size_t file_start = 0;
    size_t pattern_start = 0;
    const RowParts& pattern = row.parts;

    if (pat_->flags.windows) {
      const bool file_drive = !file.empty() && IsWindowsDrive<Char>(file[0]);
      const bool file_unc = !file_drive && file.size() > 3 && file[0].empty() &&
                            file[1].empty() && file[2].size() == 1 &&
                            file[2][0] == '?' && IsWindowsDrive<Char>(file[3]);
      const bool pattern_drive =
          !pattern.empty() && pattern[0].kind == PartMatcher::Kind::kLiteral &&
          IsWindowsDrive<char16_t>(pattern[0].literal);
      const bool pattern_unc = !pattern_drive && pattern.size() > 3 &&
                               PartIsLiteral(pattern[0], u"") &&
                               PartIsLiteral(pattern[1], u"") &&
                               PartIsLiteral(pattern[2], u"?") &&
                               pattern[3].kind == PartMatcher::Kind::kLiteral &&
                               IsWindowsDrive<char16_t>(pattern[3].literal);
      const ptrdiff_t fdi = file_unc ? 3 : file_drive ? 0 : -1;
      const ptrdiff_t pdi = pattern_unc ? 3 : pattern_drive ? 0 : -1;
      if (fdi >= 0 && pdi >= 0) {
        const View fd = file[fdi];
        const PatternString& pd = pattern[pdi].literal;
        if (fd.size() == pd.size() && AsciiLower(fd[0]) == AsciiLower(pd[0]) &&
            static_cast<uint32_t>(fd[1]) == static_cast<uint32_t>(pd[1])) {
          // minimatch rewrites its compiled drive letter to the file's and
          // matches on from there; comparing here and stepping past both is
          // the same thing without mutating the shared compiled pattern.
          pattern_start = pdi + 1;
          file_start = fdi + 1;
        }
      }
    }

    if (row.has_globstar) {
      return MatchGlobstar(file, pattern, file_start, pattern_start) ==
             Tri::kTrue;
    }
    return MatchLinear(
        file, file.size(), pattern, pattern_start, pattern.size(), file_start);
  }

  // #matchOne(file.slice(0, f_end), pattern.slice(p_begin, p_end), partial,
  //           fileIndex, 0)
  bool MatchLinear(Parts file,
                   size_t f_end,
                   const RowParts& pattern,
                   size_t p_begin,
                   size_t p_end,
                   size_t f_index) {
    size_t fi = f_index;
    size_t pi = p_begin;
    const size_t fl = f_end;
    const size_t pl = p_end;
    for (; fi < fl && pi < pl; fi++, pi++) {
      const PartMatcher& p = pattern[pi];
      if (p.kind == PartMatcher::Kind::kGlobstar) return false;
      if (fi >= file.size()) return false;  // sliced view can't exceed store
      if (!TestPartImpl(p, file[fi])) return false;
    }
    if (fi == fl && pi == pl) return true;
    if (fi == fl) return false;
    if (pi == pl) return fi == fl - 1 && fi < file.size() && file[fi].empty();
    return false;
  }

  enum class Tri { kFalse, kNull, kTrue };

  struct Section {
    size_t begin, end;  // pattern part indices
    ptrdiff_t after;    // last possible file index this section can start at
    size_t non_gs;
  };

  Tri MatchGlobstar(Parts file,
                    const RowParts& pattern,
                    size_t file_index,
                    size_t pattern_index) {
    // locate first (from pattern_index) and last globstar
    size_t firstgs = pattern_index;
    while (firstgs < pattern.size() &&
           pattern[firstgs].kind != PartMatcher::Kind::kGlobstar) {
      firstgs++;
    }
    size_t lastgs = pattern.size();
    for (size_t i = pattern.size(); i-- > 0;) {
      if (pattern[i].kind == PartMatcher::Kind::kGlobstar) {
        lastgs = i;
        break;
      }
    }
    // head, body, tail
    const size_t head_begin = pattern_index;
    const size_t head_end = firstgs;
    const size_t body_begin = firstgs + 1;
    // slice(firstgs + 1, lastgs) is empty when they are the same globstar
    const size_t body_end = std::max(lastgs, body_begin);
    const size_t tail_begin = lastgs + 1;
    const size_t tail_end = pattern.size();
    const size_t head_len = head_end - head_begin;
    size_t fi = file_index;
    if (head_len > 0) {
      // #matchOne(file.slice(fileIndex, fileIndex + head.length), head, ...)
      const size_t slice_end = std::min(file.size(), fi + head_len);
      if (!MatchLinear(file, slice_end, pattern, head_begin, head_end, fi)) {
        return Tri::kFalse;
      }
      fi += head_len;
    }
    const size_t tail_len = tail_end - tail_begin;
    size_t file_tail_match = 0;
    if (tail_len > 0) {
      if (tail_len + fi > file.size()) return Tri::kFalse;
      size_t tail_start = file.size() - tail_len;
      if (MatchLinear(
              file, file.size(), pattern, tail_begin, tail_end, tail_start)) {
        file_tail_match = tail_len;
      } else {
        // affordance: a/**/* matching a/b/
        if (file.empty() || !file.back().empty() ||
            fi + tail_len == file.size()) {
          return Tri::kFalse;
        }
        tail_start--;
        if (!MatchLinear(
                file, file.size(), pattern, tail_begin, tail_end, tail_start)) {
          return Tri::kFalse;
        }
        file_tail_match = tail_len + 1;
      }
    }
    const size_t body_len = body_end - body_begin;
    if (body_len == 0) {
      bool saw_some = file_tail_match != 0;
      const size_t scan_end =
          file.size() >= file_tail_match ? file.size() - file_tail_match : 0;
      for (size_t i = fi; i < scan_end; i++) {
        const View f = file[i];
        saw_some = true;
        if (IsDots(f) || (!pat_->flags.dot && f.starts_with('.'))) {
          return Tri::kFalse;
        }
      }
      return saw_some ? Tri::kTrue : Tri::kFalse;
    }
    // split body into globstar-delimited sections with their last possible
    // start positions
    std::vector<Section> sections;
    sections.push_back({body_begin, body_begin, 0, 0});
    size_t non_gs = 0;
    for (size_t i = body_begin; i < body_end; i++) {
      if (pattern[i].kind == PartMatcher::Kind::kGlobstar) {
        sections.push_back({i + 1, i + 1, 0, non_gs});
      } else {
        sections.back().end = i + 1;
        non_gs++;
      }
    }
    const ptrdiff_t file_length = static_cast<ptrdiff_t>(file.size()) -
                                  static_cast<ptrdiff_t>(file_tail_match);
    {
      size_t i = sections.size() - 1;
      for (Section& s : sections) {
        s.after = file_length - (static_cast<ptrdiff_t>(sections[i--].non_gs) +
                                 static_cast<ptrdiff_t>(s.end - s.begin));
      }
    }
    return MatchBodySections(
        file, pattern, sections, fi, 0, 0, file_tail_match != 0);
  }

  // #matchGlobStarBodySections
  Tri MatchBodySections(Parts file,
                        const RowParts& pattern,
                        const std::vector<Section>& sections,
                        size_t file_index,
                        size_t body_index,
                        int globstar_depth,
                        bool saw_tail) {
    if (body_index >= sections.size()) {
      // just make sure that there's no bad dots
      for (size_t i = file_index; i < file.size(); i++) {
        saw_tail = true;
        const View f = file[i];
        if (IsDots(f) || (!pat_->flags.dot && f.starts_with('.'))) {
          return Tri::kFalse;
        }
      }
      return saw_tail ? Tri::kTrue : Tri::kFalse;
    }
    const Section& bs = sections[body_index];
    const size_t body_len = bs.end - bs.begin;
    size_t fi = file_index;
    while (static_cast<ptrdiff_t>(fi) <= bs.after) {
      const size_t slice_end = std::min(file.size(), fi + body_len);
      const bool m =
          MatchLinear(file, slice_end, pattern, bs.begin, bs.end, fi);
      // if the cap is exceeded: intentional false negative, for security
      if (m && globstar_depth < kMaxGlobstarRecursion) {
        const Tri sub = MatchBodySections(file,
                                          pattern,
                                          sections,
                                          fi + body_len,
                                          body_index + 1,
                                          globstar_depth + 1,
                                          saw_tail);
        if (sub != Tri::kFalse) return sub;
      }

      if (fi >= file.size()) return Tri::kFalse;
      const View f = file[fi];
      if (IsDots(f) || (!pat_->flags.dot && f.starts_with('.'))) {
        return Tri::kFalse;
      }
      fi++;
    }
    return Tri::kNull;
  }

  const CompiledPattern* pat_;
  std::basic_string<Char> rewritten_;
  MaybeStackBuffer<View, kInlineParts> parts_;
  std::vector<std::basic_string<Char>> owned_;
  std::vector<View> owned_views_;
};

}  // namespace

bool MatchPattern(const CompiledPattern& pattern, PatternView path) {
  PathMatcher<char16_t> m(pattern);
  return m.Match(path);
}

bool MatchPattern(const CompiledPattern& pattern, OneByteView path) {
  PathMatcher<char8_t> m(pattern);
  return m.Match(path);
}

}  // namespace node::glob