PR #2135: Fix greedy star matching in log fnmatch

Imported from GitHub PR https://github.com/abseil/abseil-cpp/pull/2135

FNMatch backs the --vmodule module patterns, and after a '*' it bound the following literal run to its first occurrence in the module name and never backtracked. So a pattern like *_test failed to match a module whose stem is unit_test_test, and the requested verbosity was silently not applied; the matcher can only produce false negatives in this case, never spurious matches. This swaps the greedy single-find loop for the usual two-pointer glob match that remembers the position just after the most recent '*' and retries on a later mismatch, and adds a regression test covering recurring literal runs after a star.
Merge 46290b1cb19954bb55db9edb04b1fed8d9b55082 into e3c6ae22841722729138f0c2a23627bc2a0e0405

Merging this change closes #2135

PiperOrigin-RevId: 971938720
Change-Id: Id9e632f2febf3eda5a955104ba1df117759dd8bd
diff --git a/absl/log/internal/fnmatch.cc b/absl/log/internal/fnmatch.cc
index 26e1e57..ee77a4f 100644
--- a/absl/log/internal/fnmatch.cc
+++ b/absl/log/internal/fnmatch.cc
@@ -23,50 +23,40 @@
 ABSL_NAMESPACE_BEGIN
 namespace log_internal {
 bool FNMatch(absl::string_view pattern, absl::string_view str) {
-  bool in_wildcard_match = false;
-  while (true) {
-    if (pattern.empty()) {
-      // `pattern` is exhausted; succeed if all of `str` was consumed matching
-      // it.
-      return in_wildcard_match || str.empty();
-    }
-    if (str.empty()) {
-      // `str` is exhausted; succeed if `pattern` is empty or all '*'s.
-      return pattern.find_first_not_of('*') == pattern.npos;
-    }
-    switch (pattern.front()) {
-      case '*':
-        pattern.remove_prefix(1);
-        in_wildcard_match = true;
-        break;
-      case '?':
-        pattern.remove_prefix(1);
-        str.remove_prefix(1);
-        break;
-      default:
-        if (in_wildcard_match) {
-          absl::string_view fixed_portion = pattern;
-          const size_t end = fixed_portion.find_first_of("*?");
-          if (end != fixed_portion.npos) {
-            fixed_portion = fixed_portion.substr(0, end);
-          }
-          const size_t match = str.find(fixed_portion);
-          if (match == str.npos) {
-            return false;
-          }
-          pattern.remove_prefix(fixed_portion.size());
-          str.remove_prefix(match + fixed_portion.size());
-          in_wildcard_match = false;
-        } else {
-          if (pattern.front() != str.front()) {
-            return false;
-          }
-          pattern.remove_prefix(1);
-          str.remove_prefix(1);
-        }
-        break;
+  // Two-pointer glob matcher: '?' matches exactly one character and '*' matches
+  // any run of characters (including the empty run). We remember the position
+  // just after the most recent '*' so that, on a later mismatch, that '*' can
+  // consume one more character of `str` and the match be retried.
+  size_t p = 0;  // Current position in `pattern`.
+  size_t s = 0;  // Current position in `str`.
+  // `pattern` position just after the most recent '*', and the `str` position
+  // when it was seen; `npos` until a '*' has been encountered.
+  size_t star_p = absl::string_view::npos;
+  size_t star_s = 0;
+  while (s < str.size()) {
+    if (p < pattern.size() && pattern[p] == '*') {
+      // Found '*'. Record checkpoint after '*' and advance pattern index only.
+      star_p = ++p;
+      star_s = s;
+    } else if (p < pattern.size() &&
+               (pattern[p] == '?' || pattern[p] == str[s])) {
+      // Literal character match or single-character wildcard '?'.  Advance both
+      // pattern and string pointers.
+      ++p;
+      ++s;
+    } else if (star_p != absl::string_view::npos) {
+      // Mismatch, but a preceding '*' exists. Backtrack: reset pattern to after
+      // the '*', and let that '*' consume one more character.
+      p = star_p;
+      s = ++star_s;
+    } else {
+      // Mismatch and no preceding '*' exists to absorb it.
+      return false;
     }
   }
+  // `str` is exhausted; the remainder of `pattern` must be all '*'s.
+  while (p < pattern.size() && pattern[p] == '*') ++p;
+  return p == pattern.size();
 }
 }  // namespace log_internal
 ABSL_NAMESPACE_END
diff --git a/absl/log/internal/fnmatch_test.cc b/absl/log/internal/fnmatch_test.cc
index 614c7d3..27611e3 100644
--- a/absl/log/internal/fnmatch_test.cc
+++ b/absl/log/internal/fnmatch_test.cc
@@ -57,4 +57,19 @@
   EXPECT_THAT(FNMatch("*?", "*"), IsTrue());
 }
 
+TEST(FNMatchTest, BacktracksAfterStar) {
+  using absl::log_internal::FNMatch;
+  // A literal run after '*' may occur more than once in `str`; the match must
+  // not commit to the first occurrence.
+  EXPECT_THAT(FNMatch("*test", "testtest"), IsTrue());
+  EXPECT_THAT(FNMatch("*.cc", "aa.cc.cc"), IsTrue());
+  EXPECT_THAT(FNMatch("*_test", "unit_test_test"), IsTrue());
+  EXPECT_THAT(FNMatch("*aa", "aaa"), IsTrue());
+  EXPECT_THAT(FNMatch("*a*b", "aXaXb"), IsTrue());
+  EXPECT_THAT(FNMatch("*ab*ab", "abXab"), IsTrue());
+  // Backtracking must still reject genuine non-matches.
+  EXPECT_THAT(FNMatch("*test", "testtes"), IsFalse());
+  EXPECT_THAT(FNMatch("*_test", "unit_tests"), IsFalse());
+}
+
 }  // namespace